Compare commits
310 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebe768711b | ||
|
|
af45f93854 | ||
|
|
da1d74309e | ||
|
|
7a4d7b6915 | ||
|
|
ab7031e6ed | ||
|
|
cf3cec0164 | ||
|
|
79762c3f0d | ||
|
|
715cbc1894 | ||
|
|
dd398178f0 | ||
|
|
8b0d473fcd | ||
|
|
30e9b55379 | ||
|
|
3091754b0f | ||
|
|
26de222884 | ||
|
|
2f2f93f37e | ||
|
|
1b9a3b84a0 | ||
|
|
c77de4a232 | ||
|
|
15b8c09e17 | ||
|
|
77e87504d6 | ||
|
|
d8a21f0724 | ||
|
|
4a91ca0774 | ||
|
|
52c22f1eb9 | ||
|
|
a517785050 | ||
|
|
474738a894 | ||
|
|
41397ae067 | ||
|
|
dd91a4989b | ||
|
|
f06f5f2bb1 | ||
|
|
c8caf3dc44 | ||
|
|
2de10f6eb0 | ||
|
|
e2c0f203f9 | ||
|
|
a383b9bcf4 | ||
|
|
6aeba1fe83 | ||
|
|
7a1625c297 | ||
|
|
9d2d87e7e1 | ||
|
|
b5c19b6774 | ||
|
|
213aa86c47 | ||
|
|
b2ae37637d | ||
|
|
7e968dc06b | ||
|
|
0ec41bc02c | ||
|
|
59ddbb65b9 | ||
|
|
673f0958c5 | ||
|
|
e8039f9cc4 | ||
|
|
9eb2c2692b | ||
|
|
090c51c809 | ||
|
|
32cde5363f | ||
|
|
604e5db874 | ||
|
|
a4fcb8810f | ||
|
|
3d71345181 | ||
|
|
5103f40196 | ||
|
|
09a60c5a6c | ||
|
|
7a84914866 | ||
|
|
88c51b75bf | ||
|
|
3f87f37095 | ||
|
|
ac4476ccd6 | ||
|
|
30344d29b1 | ||
|
|
f12f5f3c8d | ||
|
|
c6f70968a2 | ||
|
|
faf5d505f4 | ||
|
|
c4b396e618 | ||
|
|
e5e6175aca | ||
|
|
0516207e83 | ||
|
|
d79e7d0168 | ||
|
|
7c88b03956 | ||
|
|
55e1c7cbb5 | ||
|
|
6a1d199da6 | ||
|
|
459f4d2e0c | ||
|
|
27249c2440 | ||
|
|
f59423bc91 | ||
|
|
e5be9f81e0 | ||
|
|
9f281c3354 | ||
|
|
f2a094f349 | ||
|
|
dd1cae6f70 | ||
|
|
7903a300db | ||
|
|
5873e6a57c | ||
|
|
816a034d4a | ||
|
|
2fade7192a | ||
|
|
175e62f514 | ||
|
|
b4c9be9334 | ||
|
|
8b276a92a7 | ||
|
|
01c6d5c131 | ||
|
|
626501cb04 | ||
|
|
3362417907 | ||
|
|
7b2622fca9 | ||
|
|
24d760af8a | ||
|
|
d32bde58e2 | ||
|
|
3d86d57a80 | ||
|
|
29f4be542b | ||
|
|
2f2e503447 | ||
|
|
7ee57ca975 | ||
|
|
c8fcee9d09 | ||
|
|
0ed30d92f1 | ||
|
|
4e59b0bcd0 | ||
|
|
eaeef6f0b2 | ||
|
|
9f0c2e1225 | ||
|
|
e934b31164 | ||
|
|
77d871c4f8 | ||
|
|
4296d41cad | ||
|
|
a5ba684c7d | ||
|
|
a658ed9135 | ||
|
|
b863370511 | ||
|
|
048f6566a9 | ||
|
|
5cb3de03af | ||
|
|
ef9d8c8066 | ||
|
|
1ca4cac396 | ||
|
|
6b06e7f86b | ||
|
|
e703a9d39d | ||
|
|
67bae5640c | ||
|
|
c06f0c89e5 | ||
|
|
73560d761d | ||
|
|
4ed804141a | ||
|
|
de2281cad2 | ||
|
|
5af20d0f63 | ||
|
|
91171590e6 | ||
|
|
699ef86f8f | ||
|
|
d63a9f8ce7 | ||
|
|
77c73fe3e6 | ||
|
|
1e6de17084 | ||
| 7ee7076eec | |||
|
|
698b97d536 | ||
|
|
4fe418cc89 | ||
|
|
66abb1fe3a | ||
|
|
611c201887 | ||
|
|
f2abe296ee | ||
|
|
fc27880d96 | ||
|
|
8219c54422 | ||
|
|
c1b156bdb4 | ||
|
|
0eb377b515 | ||
|
|
facf7fb6ef | ||
|
|
90be1875e0 | ||
|
|
065517f032 | ||
|
|
99b97c53dd | ||
|
|
79e5caaf7a | ||
|
|
5b5fa28ba0 | ||
|
|
3b2c5ccdbe | ||
|
|
c8d824d347 | ||
|
|
615a3c6e99 | ||
|
|
dbf64ecb48 | ||
|
|
1702200a60 | ||
|
|
004574d442 | ||
|
|
41111b082c | ||
|
|
e9b1c94d1a | ||
|
|
0d7d04501c | ||
|
|
6393e5096f | ||
|
|
4af71aabac | ||
|
|
acb7cade90 | ||
|
|
19d3c8fa93 | ||
|
|
990d607d4b | ||
|
|
0df7735d20 | ||
|
|
7926179ed9 | ||
|
|
1855153dbe | ||
|
|
3751762c69 | ||
|
|
56f98671ca | ||
|
|
cbe41d7ac7 | ||
|
|
bd8e95c6ce | ||
|
|
fee9b4bd83 | ||
|
|
7ec683aca0 | ||
|
|
ac750b603f | ||
|
|
5306be3f2e | ||
|
|
b0dcd0ac6b | ||
|
|
159e4adf07 | ||
|
|
085c4e395b | ||
|
|
d7166b6d0a | ||
|
|
25e23c0b87 | ||
|
|
cf9e847f84 | ||
|
|
bfd76261ef | ||
|
|
0b8efa1998 | ||
|
|
3027e9b24f | ||
|
|
3d5d462de0 | ||
|
|
f675451384 | ||
|
|
021a52e56b | ||
|
|
5ccefa3cdb | ||
|
|
e4c66b1311 | ||
|
|
5da03d0938 | ||
|
|
3af1d1f3b6 | ||
|
|
1984c597de | ||
|
|
3029704051 | ||
|
|
2b805ec196 | ||
|
|
ff59dc5d57 | ||
|
|
3928743ea7 | ||
|
|
cf6c4bd60c | ||
|
|
edd841ffeb | ||
|
|
889f0e5263 | ||
|
|
3a41e1f1d3 | ||
|
|
509159417b | ||
|
|
30c8fe1091 | ||
|
|
b1ff05439a | ||
|
|
eb9c20e734 | ||
|
|
f6220a9f89 | ||
|
|
9b7626f6ff | ||
|
|
20d1182259 | ||
|
|
afcb7d3175 | ||
|
|
ac32396a57 | ||
|
|
78e12f5cf9 | ||
|
|
62709ce80b | ||
|
|
ea88042ef5 | ||
|
|
cde79bd172 | ||
|
|
a2a8e4ae3f | ||
|
|
6e187ccb48 | ||
|
|
862a27b0b8 | ||
|
|
d6c1f2c25b | ||
|
|
100b780b47 | ||
|
|
bd63a8ce95 | ||
|
|
ef9ec6069f | ||
|
|
bf84f1814f | ||
|
|
00efaf0ca0 | ||
|
|
900b6f45c5 | ||
|
|
dd6ea7563f | ||
|
|
57bb84a2df | ||
|
|
a0fbf785c3 | ||
|
|
91e51d4f6a | ||
|
|
b52d588fc5 | ||
|
|
23b23bbb66 | ||
|
|
82b47f4364 | ||
|
|
e4b2dd2604 | ||
|
|
3b31e69ae4 | ||
|
|
499fd7f8e8 | ||
|
|
1080c76e99 | ||
|
|
7f58bca0e6 | ||
|
|
c087e4af08 | ||
|
|
387ed44989 | ||
|
|
64b677696e | ||
|
|
78813ea15f | ||
|
|
807e191397 | ||
|
|
47ff122c48 | ||
|
|
eb796f531f | ||
|
|
a3706cf7c2 | ||
|
|
2b1d49c032 | ||
|
|
ae1ee38441 | ||
|
|
d6d96aad07 | ||
|
|
2d6cc4c634 | ||
|
|
ca5250c134 | ||
|
|
64f797bd96 | ||
|
|
f08461cf35 | ||
|
|
2b5d803a60 | ||
|
|
e3902cd85f | ||
|
|
25ca8d5132 | ||
|
|
0d94132c98 | ||
|
|
0e6de69cd9 | ||
|
|
e53274bcb9 | ||
|
|
4433b26bf8 | ||
|
|
74fa08f41f | ||
|
|
4b66d78cf4 | ||
|
|
b1c2950b1e | ||
|
|
b0484459a2 | ||
|
|
056a6f0ff5 | ||
|
|
f4bf38fcba | ||
|
|
15632a2170 | ||
|
|
479b67cd2d | ||
|
|
bde0459416 | ||
|
|
a01712e68c | ||
|
|
9aa78f681d | ||
|
|
befefe457f | ||
|
|
ea665ff411 | ||
|
|
f9bd492191 | ||
|
|
1be303b801 | ||
|
|
d57249906a | ||
|
|
6a24dd01e9 | ||
|
|
e10f021c54 | ||
|
|
b3c5e87230 | ||
|
|
9b63443842 | ||
|
|
cd30c2d9b5 | ||
|
|
b612941aae | ||
|
|
20ee448f4e | ||
|
|
2bbca8ae38 | ||
|
|
fea50b51ae | ||
| 79d37118e0 | |||
|
|
7fd55ea8ba | ||
|
|
c96fbef5d5 | ||
|
|
7423e2ca14 | ||
|
|
bf600f8c5f | ||
|
|
996ea65293 | ||
|
|
9866dd5f23 | ||
|
|
d9c8816647 | ||
|
|
b32c97c02b | ||
|
|
552f02d25c | ||
|
|
9f9968abab | ||
|
|
69a3eb192f | ||
|
|
488a32f319 | ||
|
|
bf57fd139b | ||
|
|
581d53a33e | ||
|
|
f4dd2b3415 | ||
|
|
7532cc9d59 | ||
|
|
e7590d72fd | ||
|
|
57ce1db248 | ||
|
|
c97d730a00 | ||
|
|
581c4f9ad9 | ||
|
|
ef6bc4be21 | ||
|
|
8534bb8839 | ||
|
|
a5bc7cf6d1 | ||
|
|
5d2eff4f73 | ||
|
|
9a4a4dc1af | ||
|
|
f3241e904f | ||
|
|
5de792744e | ||
|
|
0a5f4a03b5 | ||
|
|
4ac11551c9 | ||
|
|
6fea5f2c5b | ||
|
|
b7cac68ee1 | ||
|
|
cdbe330c47 | ||
| 53e9073dca | |||
| b8c316727e | |||
|
|
48455cd559 | ||
| aa3d9f375b | |||
|
|
e54d20bcb7 | ||
|
|
81f85aa82d | ||
| 2887fe9599 | |||
| b1679b110c | |||
| e7835e1100 | |||
| ed65b87af2 | |||
| 4a99e6cf6b | |||
| 4d9a9ff851 | |||
| 292a38fe30 |
@@ -14,16 +14,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'delete'
|
||||
container:
|
||||
image: maven:3.9-eclipse-temurin-17
|
||||
image: gitea.siegeln.net/cameleer/cameleer-build:1
|
||||
credentials:
|
||||
username: cameleer
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
steps:
|
||||
- name: Install Node.js 22
|
||||
run: |
|
||||
apt-get update && apt-get install -y ca-certificates curl gnupg
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
|
||||
apt-get update && apt-get install -y nodejs
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure Gitea Maven Registry
|
||||
@@ -53,22 +48,27 @@ jobs:
|
||||
- name: Build UI
|
||||
working-directory: ui
|
||||
run: |
|
||||
echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc
|
||||
npm ci
|
||||
npm run build
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and Test
|
||||
run: mvn clean verify -DskipITs --batch-mode
|
||||
run: mvn clean verify -DskipITs -U --batch-mode
|
||||
|
||||
docker:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
container:
|
||||
image: docker:27
|
||||
image: gitea.siegeln.net/cameleer/cameleer-docker-builder:1
|
||||
credentials:
|
||||
username: cameleer
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git .
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
echo "IMAGE_TAGS=branch-$SLUG" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Set up QEMU for cross-platform builds
|
||||
run: docker run --rm --privileged tonistiigi/binfmt --install all
|
||||
run: docker run --rm --privileged gitea.siegeln.net/cameleer/binfmt:1 --install all
|
||||
- name: Build and push server
|
||||
run: |
|
||||
docker buildx create --use --name cibuilder
|
||||
@@ -133,7 +133,6 @@ jobs:
|
||||
if: always()
|
||||
- name: Cleanup old container images
|
||||
run: |
|
||||
apk add --no-cache curl jq
|
||||
API="https://gitea.siegeln.net/api/v1"
|
||||
AUTH="Authorization: token ${REGISTRY_TOKEN}"
|
||||
CURRENT_SHA="${{ github.sha }}"
|
||||
|
||||
89
.gitea/workflows/sonarqube.yml
Normal file
89
.gitea/workflows/sonarqube.yml
Normal file
@@ -0,0 +1,89 @@
|
||||
name: SonarQube
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: gitea.siegeln.net/cameleer/cameleer-build:1
|
||||
credentials:
|
||||
username: cameleer
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Gitea Maven Registry
|
||||
run: |
|
||||
mkdir -p ~/.m2
|
||||
cat > ~/.m2/settings.xml << 'SETTINGS'
|
||||
<settings>
|
||||
<servers>
|
||||
<server>
|
||||
<id>gitea</id>
|
||||
<username>cameleer</username>
|
||||
<password>${env.REGISTRY_TOKEN}</password>
|
||||
</server>
|
||||
</servers>
|
||||
</settings>
|
||||
SETTINGS
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Cache Maven dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-maven-
|
||||
|
||||
- name: Build and Test Java
|
||||
run: mvn clean verify -DskipITs -U --batch-mode
|
||||
|
||||
- name: Install UI dependencies
|
||||
working-directory: ui
|
||||
run: |
|
||||
echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc
|
||||
npm ci
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Lint UI
|
||||
working-directory: ui
|
||||
run: npm run lint -- --format json --output-file eslint-report.json || true
|
||||
|
||||
- name: Install sonar-scanner
|
||||
run: |
|
||||
SONAR_SCANNER_VERSION=6.2.1.4610
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
aarch64|arm64) PLATFORM="linux-aarch64" ;;
|
||||
*) PLATFORM="linux-x64" ;;
|
||||
esac
|
||||
curl -sSLo sonar-scanner.zip "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-${PLATFORM}.zip"
|
||||
unzip -q sonar-scanner.zip
|
||||
ln -s "$(pwd)/sonar-scanner-${SONAR_SCANNER_VERSION}-${PLATFORM}/bin/sonar-scanner" /usr/local/bin/sonar-scanner
|
||||
|
||||
- name: SonarQube Analysis
|
||||
run: |
|
||||
sonar-scanner \
|
||||
-Dsonar.host.url="$SONAR_HOST_URL" \
|
||||
-Dsonar.token="$SONAR_TOKEN" \
|
||||
-Dsonar.projectKey=cameleer3-server \
|
||||
-Dsonar.projectName="Cameleer3 Server" \
|
||||
-Dsonar.sources=cameleer3-server-core/src/main/java,cameleer3-server-app/src/main/java,ui/src \
|
||||
-Dsonar.tests=cameleer3-server-core/src/test/java,cameleer3-server-app/src/test/java \
|
||||
-Dsonar.java.binaries=cameleer3-server-core/target/classes,cameleer3-server-app/target/classes \
|
||||
-Dsonar.java.test.binaries=cameleer3-server-core/target/test-classes,cameleer3-server-app/target/test-classes \
|
||||
-Dsonar.java.libraries="$HOME/.m2/repository/**/*.jar" \
|
||||
-Dsonar.typescript.eslint.reportPaths=ui/eslint-report.json \
|
||||
-Dsonar.eslint.reportPaths=ui/eslint-report.json \
|
||||
-Dsonar.exclusions="ui/node_modules/**,ui/dist/**,**/target/**"
|
||||
env:
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ logs/
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
.worktrees/
|
||||
|
||||
BIN
.playwright-mcp/page-2026-03-24T18-41-08-535Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-24T18-41-08-535Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
.playwright-mcp/page-2026-03-24T18-41-32-291Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-24T18-41-32-291Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
.playwright-mcp/page-2026-03-24T19-34-41-650Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-24T19-34-41-650Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
.playwright-mcp/page-2026-03-24T19-51-20-426Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-24T19-51-20-426Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
.playwright-mcp/page-2026-03-24T19-53-00-560Z.png
Normal file
BIN
.playwright-mcp/page-2026-03-24T19-53-00-560Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
1
.superpowers/brainstorm/10188-1774613058/.server-stopped
Normal file
1
.superpowers/brainstorm/10188-1774613058/.server-stopped
Normal file
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1774616238650}
|
||||
1
.superpowers/brainstorm/10188-1774613058/.server.pid
Normal file
1
.superpowers/brainstorm/10188-1774613058/.server.pid
Normal file
@@ -0,0 +1 @@
|
||||
10188
|
||||
@@ -0,0 +1,105 @@
|
||||
<h2>ProcessDiagram Component Hierarchy</h2>
|
||||
<p class="subtitle">How the SVG rendering is structured — from data fetch to pixels</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Component Tree</div>
|
||||
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.8; color: #1A1612;">
|
||||
<div><strong style="color: #1A7F8E;">ProcessDiagram</strong> — root, fetches layout, manages state</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
|
||||
<div><svg> container with viewBox (zoom/pan transforms)</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
|
||||
<div><strong style="color: #7C3AED;">DiagramSection</strong> label="Main Route"</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
|
||||
<div><strong style="color: #9C9184;"><g></strong> edges layer (rendered first, behind nodes)</div>
|
||||
<div style="padding-left: 24px;">
|
||||
<div><strong style="color: #C6820E;">DiagramEdge</strong> × N — SVG <path> with arrowhead</div>
|
||||
</div>
|
||||
<div><strong style="color: #9C9184;"><g></strong> nodes layer</div>
|
||||
<div style="padding-left: 24px;">
|
||||
<div><strong style="color: #C6820E;">DiagramNode</strong> × N — top-bar card</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
|
||||
<div><strong style="color: #3D7C47;">ConfigBadge</strong> × 0..N — tap/trace indicators</div>
|
||||
<div><strong style="color: #3D7C47;">NodeToolbar</strong> — floating on hover</div>
|
||||
</div>
|
||||
<div><strong style="color: #C6820E;">CompoundNode</strong> × 0..N — choice/split container</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
|
||||
<div><strong style="color: #C6820E;">DiagramNode</strong> × N — children inside compound</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px;"><strong style="color: #C0392B;">DiagramSection</strong> label="onException" variant="error"</div>
|
||||
<div style="padding-left: 24px; border-left: 2px solid #C0392B;">
|
||||
<div><em style="color: #9C9184;">same edge + node structure as above</em></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px;"><strong style="color: #1A7F8E;">ZoomControls</strong> — HTML overlay (not SVG)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top: 24px;">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">SVG Structure (simplified)</div>
|
||||
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7; color: #5C5347; background: #F5F2ED;">
|
||||
<pre style="margin: 0;"><div class="process-diagram"> <span style="color:#9C9184">/* wrapper div */</span>
|
||||
<svg viewBox="0 0 {w} {h}"> <span style="color:#9C9184">/* zoom = viewBox transform */</span>
|
||||
<g class="diagram-content"> <span style="color:#9C9184">/* pan offset */</span>
|
||||
|
||||
<span style="color:#7C3AED"><!-- Main Route section --></span>
|
||||
<g class="section section--main">
|
||||
<g class="edges">
|
||||
<path d="M 100 40 C ..." /> <span style="color:#9C9184">/* cubic bezier edge */</span>
|
||||
<marker>...</marker> <span style="color:#9C9184">/* arrowhead def */</span>
|
||||
</g>
|
||||
<g class="nodes">
|
||||
<g transform="translate(x, y)"> <span style="color:#9C9184">/* positioned by ELK */</span>
|
||||
<rect .../> <span style="color:#9C9184">/* card background */</span>
|
||||
<rect .../> <span style="color:#9C9184">/* color top bar */</span>
|
||||
<text>LOG</text> <span style="color:#9C9184">/* label */</span>
|
||||
<g class="badges">...</g> <span style="color:#9C9184">/* config indicators */</span>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<span style="color:#C0392B"><!-- Error Handler section --></span>
|
||||
<g class="section section--error"
|
||||
transform="translate(0, {mainH + gap})">
|
||||
<text>onException</text> <span style="color:#9C9184">/* section label */</span>
|
||||
<line .../> <span style="color:#9C9184">/* divider line */</span>
|
||||
<g class="edges">...</g>
|
||||
<g class="nodes">...</g>
|
||||
</g>
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
<div class="zoom-controls">...</div> <span style="color:#9C9184">/* HTML overlay */</span>
|
||||
</div></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top: 24px;">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Data Flow</div>
|
||||
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.8; color: #5C5347;">
|
||||
<pre style="margin: 0;">
|
||||
<span style="color:#1A7F8E">GET /diagrams/{hash}/render?direction=LR</span>
|
||||
│
|
||||
▼
|
||||
DiagramLayout { nodes[], edges[], width, height }
|
||||
│
|
||||
▼
|
||||
<span style="color:#7C3AED">separateFlows(nodes)</span> → mainNodes[] + errorSections[]
|
||||
│ │
|
||||
▼ ▼
|
||||
<span style="color:#C6820E">renderMainSection()</span> <span style="color:#C0392B">renderErrorSection()</span>
|
||||
│ │
|
||||
▼ ▼
|
||||
SVG groups with SVG groups offset below
|
||||
ELK x/y coordinates main section by mainHeight + gap
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,164 @@
|
||||
<h2>Node Interactions & Config Badges</h2>
|
||||
<p class="subtitle">Hover toolbar, selection states, and active config indicators</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Node States</div>
|
||||
<div class="mockup-body" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="340" viewBox="0 0 520 340">
|
||||
|
||||
<!-- 1. Normal state -->
|
||||
<text x="10" y="16" fill="#9C9184" font-size="11" font-weight="600">NORMAL</text>
|
||||
<g transform="translate(10, 24)">
|
||||
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
</g>
|
||||
|
||||
<!-- 2. Hovered state with toolbar -->
|
||||
<text x="270" y="16" fill="#9C9184" font-size="11" font-weight="600">HOVERED (toolbar appears)</text>
|
||||
<g transform="translate(270, 24)">
|
||||
<rect x="0" y="0" width="200" height="56" rx="4" fill="#FFFCF5" stroke="#C6820E" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<!-- Floating toolbar -->
|
||||
<g transform="translate(30, -32)">
|
||||
<rect x="0" y="0" width="140" height="28" rx="6" fill="#1A1612" opacity="0.92"/>
|
||||
<!-- Icons as circles -->
|
||||
<g transform="translate(10, 4)">
|
||||
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
|
||||
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">🔍</text>
|
||||
</g>
|
||||
<g transform="translate(40, 4)">
|
||||
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
|
||||
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">T</text>
|
||||
</g>
|
||||
<g transform="translate(70, 4)">
|
||||
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
|
||||
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">✎</text>
|
||||
</g>
|
||||
<g transform="translate(100, 4)">
|
||||
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
|
||||
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">⋯</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 3. Selected state -->
|
||||
<text x="10" y="112" fill="#9C9184" font-size="11" font-weight="600">SELECTED (click)</text>
|
||||
<g transform="translate(10, 120)">
|
||||
<rect x="-2" y="-2" width="204" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5" stroke-dasharray="none"/>
|
||||
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
</g>
|
||||
|
||||
<!-- 4. With config badges -->
|
||||
<text x="270" y="112" fill="#9C9184" font-size="11" font-weight="600">WITH CONFIG BADGES</text>
|
||||
<g transform="translate(270, 120)">
|
||||
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<!-- Trace badge (top-right corner) -->
|
||||
<g transform="translate(165, -6)">
|
||||
<rect x="0" y="0" width="38" height="16" rx="8" fill="#1A7F8E"/>
|
||||
<text x="19" y="12" fill="white" font-size="8" font-weight="600" text-anchor="middle">TRACE</text>
|
||||
</g>
|
||||
<!-- Tap badge -->
|
||||
<g transform="translate(124, -6)">
|
||||
<rect x="0" y="0" width="36" height="16" rx="8" fill="#7C3AED"/>
|
||||
<text x="18" y="12" fill="white" font-size="8" font-weight="600" text-anchor="middle">TAP</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 5. Error node style -->
|
||||
<text x="10" y="210" fill="#9C9184" font-size="11" font-weight="600">ERROR HANDLER NODE</text>
|
||||
<g transform="translate(10, 218)">
|
||||
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C0392B"/>
|
||||
<rect x="4" y="0" width="192" height="6" fill="#C0392B"/>
|
||||
<text x="16" y="32" fill="#C0392B" font-size="14">⚠</text>
|
||||
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">ON_EXCEPTION</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">java.lang.Exception</text>
|
||||
</g>
|
||||
|
||||
<!-- 6. Compound node (Choice) -->
|
||||
<text x="270" y="210" fill="#9C9184" font-size="11" font-weight="600">COMPOUND NODE (CHOICE)</text>
|
||||
<g transform="translate(270, 218)">
|
||||
<rect x="0" y="0" width="220" height="110" rx="4" fill="white" stroke="#7C3AED" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="220" height="22" rx="4" fill="#7C3AED"/>
|
||||
<rect x="4" y="4" width="212" height="18" fill="#7C3AED"/>
|
||||
<text x="110" y="16" fill="white" font-size="10" font-weight="600" text-anchor="middle">◆ CHOICE</text>
|
||||
<!-- Children -->
|
||||
<g transform="translate(10, 30)">
|
||||
<rect x="0" y="0" width="200" height="32" rx="3" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="4" rx="3" fill="#7C3AED"/>
|
||||
<rect x="3" y="0" width="194" height="4" fill="#7C3AED"/>
|
||||
<text x="12" y="22" fill="#7C3AED" font-size="10">◆</text>
|
||||
<text x="28" y="22" fill="#1A1612" font-size="10" font-weight="600">WHEN</text>
|
||||
<text x="66" y="22" fill="#5C5347" font-size="9">type == 'A'</text>
|
||||
</g>
|
||||
<g transform="translate(10, 70)">
|
||||
<rect x="0" y="0" width="200" height="32" rx="3" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="200" height="4" rx="3" fill="#7C3AED"/>
|
||||
<rect x="3" y="0" width="194" height="4" fill="#7C3AED"/>
|
||||
<text x="12" y="22" fill="#7C3AED" font-size="10">◆</text>
|
||||
<text x="28" y="22" fill="#1A1612" font-size="10" font-weight="600">OTHERWISE</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top: 24px;">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Toolbar Actions</div>
|
||||
<div class="mockup-body" style="padding: 16px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 2px solid #E4DFD8;">
|
||||
<th style="text-align: left; padding: 8px; color: #5C5347;">Icon</th>
|
||||
<th style="text-align: left; padding: 8px; color: #5C5347;">Action</th>
|
||||
<th style="text-align: left; padding: 8px; color: #5C5347;">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom: 1px solid #EDE9E3;">
|
||||
<td style="padding: 8px;">🔍</td>
|
||||
<td style="padding: 8px; font-weight: 600;">Inspect</td>
|
||||
<td style="padding: 8px; color: #5C5347;">Select node & open detail side-panel</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #EDE9E3;">
|
||||
<td style="padding: 8px;">T</td>
|
||||
<td style="padding: 8px; font-weight: 600;">Toggle Trace</td>
|
||||
<td style="padding: 8px; color: #5C5347;">Enable/disable capture of input+output for this processor</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #EDE9E3;">
|
||||
<td style="padding: 8px;">✎</td>
|
||||
<td style="padding: 8px; font-weight: 600;">Configure Tap</td>
|
||||
<td style="padding: 8px; color: #5C5347;">Open tap expression editor for this processor</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px;">⋯</td>
|
||||
<td style="padding: 8px; font-weight: 600;">More</td>
|
||||
<td style="padding: 8px; color: #5C5347;">Copy processor ID, jump to code, view in search</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
119
.superpowers/brainstorm/10188-1774613058/node-interactions.html
Normal file
119
.superpowers/brainstorm/10188-1774613058/node-interactions.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<h2>Node Interaction Model</h2>
|
||||
<p class="subtitle">What happens when you interact with a processor node on the diagram?</p>
|
||||
|
||||
<div class="cards">
|
||||
<!-- Option A: Click-to-select + context menu -->
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="180" viewBox="0 0 420 180">
|
||||
<!-- Normal node -->
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<text x="96" y="72" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">normal state</text>
|
||||
</g>
|
||||
|
||||
<!-- Selected node (amber ring) -->
|
||||
<g transform="translate(10, 100)">
|
||||
<rect x="-2" y="-2" width="184" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5"/>
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">click = select</text>
|
||||
</g>
|
||||
|
||||
<!-- Context menu on right-click -->
|
||||
<g transform="translate(220, 10)">
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<!-- Context menu -->
|
||||
<g transform="translate(100, 40)">
|
||||
<rect x="0" y="0" width="140" height="96" rx="6" fill="white" stroke="#E4DFD8" stroke-width="1" filter="url(#shadow)"/>
|
||||
<text x="12" y="20" fill="#1A1612" font-size="11">🔍 View Snapshot</text>
|
||||
<line x1="8" y1="28" x2="132" y2="28" stroke="#EDE9E3" stroke-width="1"/>
|
||||
<text x="12" y="44" fill="#1A7F8E" font-size="11">⚙ Enable Tracing</text>
|
||||
<text x="12" y="64" fill="#1A7F8E" font-size="11">📌 Set Tap</text>
|
||||
<line x1="8" y1="72" x2="132" y2="72" stroke="#EDE9E3" stroke-width="1"/>
|
||||
<text x="12" y="88" fill="#5C5347" font-size="11">📋 Copy Processor ID</text>
|
||||
</g>
|
||||
<text x="90" y="152" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">right-click = context menu</text>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<filter id="shadow" x="-4" y="-2" width="148" height="104">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="0.12"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A: Click-Select + Right-Click Menu</h3>
|
||||
<p>Click to select a node (amber highlight ring). Right-click for context menu with tracing/tap/snapshot actions. Clean separation of concerns. Standard desktop UX.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option B: Hover toolbar -->
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="180" viewBox="0 0 420 180">
|
||||
<!-- Normal node -->
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<text x="96" y="72" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">normal state</text>
|
||||
</g>
|
||||
|
||||
<!-- Hovered node with floating toolbar -->
|
||||
<g transform="translate(10, 100)">
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="#FFFCF5" stroke="#C6820E" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<!-- Floating toolbar above -->
|
||||
<g transform="translate(20, -30)">
|
||||
<rect x="0" y="0" width="140" height="26" rx="13" fill="#1A1612" opacity="0.9"/>
|
||||
<text x="18" y="17" fill="white" font-size="12" title="View">🔍</text>
|
||||
<text x="46" y="17" fill="white" font-size="12" title="Trace">⚙</text>
|
||||
<text x="74" y="17" fill="white" font-size="12" title="Tap">📌</text>
|
||||
<text x="102" y="17" fill="white" font-size="12" title="Copy">📋</text>
|
||||
<text x="124" y="17" fill="white" font-size="12" title="More">⋯</text>
|
||||
</g>
|
||||
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">hover = toolbar appears</text>
|
||||
</g>
|
||||
|
||||
<!-- Click = select (same as A) -->
|
||||
<g transform="translate(220, 50)">
|
||||
<rect x="-2" y="-2" width="184" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5"/>
|
||||
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
|
||||
<text x="16" y="32" fill="#C6820E" font-size="14">⚙</text>
|
||||
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
|
||||
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
|
||||
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">click = select</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B: Hover Floating Toolbar</h3>
|
||||
<p>Hover reveals a dark floating icon toolbar above the node. Click still selects. More discoverable than right-click, but can feel cluttered on dense diagrams.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
208
.superpowers/brainstorm/10188-1774613058/node-style.html
Normal file
208
.superpowers/brainstorm/10188-1774613058/node-style.html
Normal file
@@ -0,0 +1,208 @@
|
||||
<h2>Node Visual Style</h2>
|
||||
<p class="subtitle">Which processor node style fits our design system best? Think MuleSoft / TIBCO BW5 but adapted to our warm parchment theme.</p>
|
||||
|
||||
<div class="cards">
|
||||
<!-- Option A: Icon-first blocks (MuleSoft-inspired) -->
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="220" viewBox="0 0 400 220">
|
||||
<!-- FROM node -->
|
||||
<g transform="translate(20, 10)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="8" fill="#1A7F8E" opacity="0.12" stroke="#1A7F8E" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="42" height="56" rx="8" fill="#1A7F8E"/>
|
||||
<rect x="8" y="0" width="34" height="56" fill="#1A7F8E"/>
|
||||
<text x="21" y="34" fill="white" font-size="20" text-anchor="middle">▶</text>
|
||||
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">FROM</text>
|
||||
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">direct:orders</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="66" x2="100" y2="86" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,82 100,90 105,82" fill="#9C9184"/>
|
||||
<!-- PROCESS node -->
|
||||
<g transform="translate(20, 90)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="8" fill="#C6820E" opacity="0.12" stroke="#C6820E" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="42" height="56" rx="8" fill="#C6820E"/>
|
||||
<rect x="8" y="0" width="34" height="56" fill="#C6820E"/>
|
||||
<text x="21" y="34" fill="white" font-size="18" text-anchor="middle">⚙</text>
|
||||
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">LOG</text>
|
||||
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">Processing order</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="146" x2="100" y2="166" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,162 100,170 105,162" fill="#9C9184"/>
|
||||
<!-- TO node -->
|
||||
<g transform="translate(20, 170)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="8" fill="#3D7C47" opacity="0.12" stroke="#3D7C47" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="42" height="56" rx="8" fill="#3D7C47"/>
|
||||
<rect x="8" y="0" width="34" height="56" fill="#3D7C47"/>
|
||||
<text x="21" y="34" fill="white" font-size="18" text-anchor="middle">◼</text>
|
||||
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">TO</text>
|
||||
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">kafka:processed</text>
|
||||
</g>
|
||||
|
||||
<!-- CHOICE compound on the right -->
|
||||
<g transform="translate(210, 10)">
|
||||
<rect x="0" y="0" width="180" height="210" rx="10" fill="#7C3AED" opacity="0.06" stroke="#7C3AED" stroke-width="1.5" stroke-dasharray="4 2"/>
|
||||
<text x="10" y="20" fill="#7C3AED" font-size="11" font-weight="600">CHOICE</text>
|
||||
<!-- When child -->
|
||||
<g transform="translate(10, 30)">
|
||||
<rect x="0" y="0" width="160" height="48" rx="6" fill="#7C3AED" opacity="0.12" stroke="#7C3AED" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="36" height="48" rx="6" fill="#7C3AED"/>
|
||||
<rect x="6" y="0" width="30" height="48" fill="#7C3AED"/>
|
||||
<text x="18" y="30" fill="white" font-size="14" text-anchor="middle">◆</text>
|
||||
<text x="96" y="20" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">WHEN</text>
|
||||
<text x="96" y="36" fill="#5C5347" font-size="10" text-anchor="middle">header.type == 'A'</text>
|
||||
</g>
|
||||
<!-- Otherwise child -->
|
||||
<g transform="translate(10, 88)">
|
||||
<rect x="0" y="0" width="160" height="48" rx="6" fill="#7C3AED" opacity="0.12" stroke="#7C3AED" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="36" height="48" rx="6" fill="#7C3AED"/>
|
||||
<rect x="6" y="0" width="30" height="48" fill="#7C3AED"/>
|
||||
<text x="18" y="30" fill="white" font-size="14" text-anchor="middle">◆</text>
|
||||
<text x="96" y="20" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">OTHERWISE</text>
|
||||
<text x="96" y="36" fill="#5C5347" font-size="10" text-anchor="middle">default branch</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A: Icon Sidebar Blocks</h3>
|
||||
<p>MuleSoft-style: colored icon strip on the left, label + detail on the right. Color encodes node type. Compound nodes (choice, split) use dashed containers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option B: Rounded pill with centered icon -->
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="220" viewBox="0 0 400 220">
|
||||
<!-- FROM node -->
|
||||
<g transform="translate(20, 10)">
|
||||
<rect x="0" y="0" width="160" height="50" rx="25" fill="#1A7F8E" opacity="0.15" stroke="#1A7F8E" stroke-width="1.5"/>
|
||||
<circle cx="30" cy="25" r="16" fill="#1A7F8E"/>
|
||||
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">▶</text>
|
||||
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">FROM</text>
|
||||
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">direct:orders</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="60" x2="100" y2="80" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,76 100,84 105,76" fill="#9C9184"/>
|
||||
<!-- PROCESS node -->
|
||||
<g transform="translate(20, 84)">
|
||||
<rect x="0" y="0" width="160" height="50" rx="25" fill="#C6820E" opacity="0.15" stroke="#C6820E" stroke-width="1.5"/>
|
||||
<circle cx="30" cy="25" r="16" fill="#C6820E"/>
|
||||
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">⚙</text>
|
||||
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">LOG</text>
|
||||
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">Processing order</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="134" x2="100" y2="154" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,150 100,158 105,150" fill="#9C9184"/>
|
||||
<!-- TO node -->
|
||||
<g transform="translate(20, 158)">
|
||||
<rect x="0" y="0" width="160" height="50" rx="25" fill="#3D7C47" opacity="0.15" stroke="#3D7C47" stroke-width="1.5"/>
|
||||
<circle cx="30" cy="25" r="16" fill="#3D7C47"/>
|
||||
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">◼</text>
|
||||
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">TO</text>
|
||||
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">kafka:processed</text>
|
||||
</g>
|
||||
|
||||
<!-- CHOICE compound on the right -->
|
||||
<g transform="translate(210, 10)">
|
||||
<rect x="0" y="0" width="180" height="200" rx="12" fill="#7C3AED" opacity="0.06" stroke="#7C3AED" stroke-width="1.5" stroke-dasharray="5 3"/>
|
||||
<text x="90" y="20" fill="#7C3AED" font-size="11" font-weight="600" text-anchor="middle">CHOICE</text>
|
||||
<!-- When child -->
|
||||
<g transform="translate(10, 30)">
|
||||
<rect x="0" y="0" width="160" height="44" rx="22" fill="#7C3AED" opacity="0.15" stroke="#7C3AED" stroke-width="1"/>
|
||||
<circle cx="26" cy="22" r="14" fill="#7C3AED"/>
|
||||
<text x="26" y="28" fill="white" font-size="12" text-anchor="middle">◆</text>
|
||||
<text x="96" y="18" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">WHEN</text>
|
||||
<text x="96" y="34" fill="#5C5347" font-size="10" text-anchor="middle">type == 'A'</text>
|
||||
</g>
|
||||
<!-- Otherwise child -->
|
||||
<g transform="translate(10, 84)">
|
||||
<rect x="0" y="0" width="160" height="44" rx="22" fill="#7C3AED" opacity="0.15" stroke="#7C3AED" stroke-width="1"/>
|
||||
<circle cx="26" cy="22" r="14" fill="#7C3AED"/>
|
||||
<text x="26" y="28" fill="white" font-size="12" text-anchor="middle">◆</text>
|
||||
<text x="96" y="18" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">OTHERWISE</text>
|
||||
<text x="96" y="34" fill="#5C5347" font-size="10" text-anchor="middle">default</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B: Rounded Pills</h3>
|
||||
<p>Softer, more modern look with pill-shaped nodes and circular icons. Lighter feel. Compounds still use dashed containers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option C: TIBCO BW5 style - rectangular with top color bar -->
|
||||
<div class="card" data-choice="c" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
|
||||
<svg width="100%" height="220" viewBox="0 0 400 220">
|
||||
<!-- FROM node -->
|
||||
<g transform="translate(20, 10)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="160" height="6" rx="4" fill="#1A7F8E"/>
|
||||
<rect x="4" y="0" width="152" height="6" fill="#1A7F8E"/>
|
||||
<text x="18" y="32" fill="#1A7F8E" font-size="16">▶</text>
|
||||
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">FROM</text>
|
||||
<text x="40" y="46" fill="#5C5347" font-size="10">direct:orders</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="66" x2="100" y2="86" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,82 100,90 105,82" fill="#9C9184"/>
|
||||
<!-- PROCESS node -->
|
||||
<g transform="translate(20, 90)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="160" height="6" rx="4" fill="#C6820E"/>
|
||||
<rect x="4" y="0" width="152" height="6" fill="#C6820E"/>
|
||||
<text x="18" y="32" fill="#C6820E" font-size="16">⚙</text>
|
||||
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">LOG</text>
|
||||
<text x="40" y="46" fill="#5C5347" font-size="10">Processing order</text>
|
||||
</g>
|
||||
<!-- Connector -->
|
||||
<line x1="100" y1="146" x2="100" y2="166" stroke="#9C9184" stroke-width="1.5"/>
|
||||
<polygon points="95,162 100,170 105,162" fill="#9C9184"/>
|
||||
<!-- TO node -->
|
||||
<g transform="translate(20, 170)">
|
||||
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="160" height="6" rx="4" fill="#3D7C47"/>
|
||||
<rect x="4" y="0" width="152" height="6" fill="#3D7C47"/>
|
||||
<text x="18" y="32" fill="#3D7C47" font-size="16">◼</text>
|
||||
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">TO</text>
|
||||
<text x="40" y="46" fill="#5C5347" font-size="10">kafka:processed</text>
|
||||
</g>
|
||||
|
||||
<!-- CHOICE compound on the right -->
|
||||
<g transform="translate(210, 10)">
|
||||
<rect x="0" y="0" width="180" height="210" rx="4" fill="white" stroke="#7C3AED" stroke-width="1.5"/>
|
||||
<rect x="0" y="0" width="180" height="22" rx="4" fill="#7C3AED"/>
|
||||
<rect x="4" y="4" width="172" height="18" fill="#7C3AED"/>
|
||||
<text x="90" y="16" fill="white" font-size="11" font-weight="600" text-anchor="middle">CHOICE</text>
|
||||
<!-- When child -->
|
||||
<g transform="translate(10, 32)">
|
||||
<rect x="0" y="0" width="160" height="48" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="160" height="5" rx="4" fill="#7C3AED"/>
|
||||
<rect x="4" y="0" width="152" height="5" fill="#7C3AED"/>
|
||||
<text x="14" y="28" fill="#7C3AED" font-size="14">◆</text>
|
||||
<text x="34" y="26" fill="#1A1612" font-size="11" font-weight="600">WHEN</text>
|
||||
<text x="34" y="40" fill="#5C5347" font-size="10">type == 'A'</text>
|
||||
</g>
|
||||
<!-- Otherwise child -->
|
||||
<g transform="translate(10, 90)">
|
||||
<rect x="0" y="0" width="160" height="48" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="160" height="5" rx="4" fill="#7C3AED"/>
|
||||
<rect x="4" y="0" width="152" height="5" fill="#7C3AED"/>
|
||||
<text x="14" y="28" fill="#7C3AED" font-size="14">◆</text>
|
||||
<text x="34" y="26" fill="#1A1612" font-size="11" font-weight="600">OTHERWISE</text>
|
||||
<text x="34" y="40" fill="#5C5347" font-size="10">default</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>C: Top-Bar Cards</h3>
|
||||
<p>TIBCO BW5-inspired: white cards with colored top accent bar. Clean, professional, card-like. Compound nodes get a full colored header bar with white title text.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
3
.superpowers/brainstorm/10188-1774613058/waiting.html
Normal file
3
.superpowers/brainstorm/10188-1774613058/waiting.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
1
.superpowers/brainstorm/14618-1774629192/.server-stopped
Normal file
1
.superpowers/brainstorm/14618-1774629192/.server-stopped
Normal file
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1774632733532}
|
||||
1
.superpowers/brainstorm/14618-1774629192/.server.pid
Normal file
1
.superpowers/brainstorm/14618-1774629192/.server.pid
Normal file
@@ -0,0 +1 @@
|
||||
14618
|
||||
287
.superpowers/brainstorm/14618-1774629192/detail-panel-tabs.html
Normal file
287
.superpowers/brainstorm/14618-1774629192/detail-panel-tabs.html
Normal file
@@ -0,0 +1,287 @@
|
||||
<h2>Detail Panel: Tab Designs</h2>
|
||||
<p class="subtitle">Bottom panel content when a processor node is selected</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Info Tab — processor metadata + attributes</div>
|
||||
<div class="mockup-body" style="background: #fff; padding: 0;">
|
||||
<!-- Processor header -->
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">bean:validate</span>
|
||||
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">processor-5</span>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; cursor: pointer;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
<!-- Info content -->
|
||||
<div style="padding: 12px 14px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px 24px; font-size: 12px;">
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Processor ID</div>
|
||||
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">processor-5</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Type</div>
|
||||
<div style="color: #1A1612;">BEAN</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Status</div>
|
||||
<div style="color: #C0392B; font-weight: 500;">FAILED</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Start Time</div>
|
||||
<div style="color: #1A1612;">14:32:05.123</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">End Time</div>
|
||||
<div style="color: #1A1612;">14:32:05.243</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Duration</div>
|
||||
<div style="color: #1A1612; font-weight: 500;">120ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Endpoint URI</div>
|
||||
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">bean:orderValidator?method=validate</div>
|
||||
</div>
|
||||
<div style="grid-column: span 2;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Resolved URI</div>
|
||||
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">bean://com.example.OrderValidator?method=validate</div>
|
||||
</div>
|
||||
<!-- Attributes from taps -->
|
||||
<div style="grid-column: span 3; border-top: 1px solid #E4DFD8; padding-top: 8px; margin-top: 4px;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;">Attributes</div>
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
||||
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">orderId: <strong>ORD-1234</strong></span>
|
||||
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">customer: <strong>Acme Corp</strong></span>
|
||||
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">priority: <strong>HIGH</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Headers Tab — input vs output side by side</div>
|
||||
<div class="mockup-body" style="background: #fff; padding: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
|
||||
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">processor-2</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
<!-- Headers side by side -->
|
||||
<div style="display: flex; gap: 0; padding: 0;">
|
||||
<!-- Input headers -->
|
||||
<div style="flex: 1; padding: 10px 14px; border-right: 1px solid #E4DFD8;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Input Headers</div>
|
||||
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; width: 40%;">Content-Type</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">application/json</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">JMSMessageID</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">ID:broker-42</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">breadcrumbId</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">abc-123-def</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">CamelHttpMethod</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">POST</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Output headers -->
|
||||
<div style="flex: 1; padding: 10px 14px;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Output Headers</div>
|
||||
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; width: 40%;">Content-Type</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">application/json</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">JMSMessageID</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">ID:broker-42</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">breadcrumbId</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">abc-123-def</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #F5F0EA;">
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">CamelHttpMethod</td>
|
||||
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">POST</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; color: #3D7C47;">orderStatus</td>
|
||||
<td style="padding: 3px 0; color: #3D7C47; font-family: monospace; font-size: 10px;">validated <span style="font-size: 9px; color: #9C9184; font-family: sans-serif;">(new)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Input Tab — formatted message body</div>
|
||||
<div class="mockup-body" style="background: #fff; padding: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
|
||||
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">processor-2 · 5ms</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
<!-- Body content with syntax highlighting -->
|
||||
<div style="padding: 10px 14px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
|
||||
<span style="font-size: 10px; color: #9C9184;">JSON · 234 bytes</span>
|
||||
<button style="font-size: 9px; padding: 2px 8px; border: 1px solid #E4DFD8; background: #FAFAF8; border-radius: 3px; cursor: pointer; color: #5C5347;">Copy</button>
|
||||
</div>
|
||||
<pre style="font-size: 11px; background: #1A1612; color: #E4DFD8; padding: 12px; border-radius: 6px; margin: 0; line-height: 1.6; overflow-x: auto;">{
|
||||
<span style="color: #1A7F8E;">"orderId"</span>: <span style="color: #C6820E;">"ORD-1234"</span>,
|
||||
<span style="color: #1A7F8E;">"customer"</span>: {
|
||||
<span style="color: #1A7F8E;">"name"</span>: <span style="color: #C6820E;">"Acme Corp"</span>,
|
||||
<span style="color: #1A7F8E;">"id"</span>: <span style="color: #7C3AED;">42</span>
|
||||
},
|
||||
<span style="color: #1A7F8E;">"items"</span>: [
|
||||
{
|
||||
<span style="color: #1A7F8E;">"product"</span>: <span style="color: #C6820E;">"Widget A"</span>,
|
||||
<span style="color: #1A7F8E;">"quantity"</span>: <span style="color: #7C3AED;">5</span>,
|
||||
<span style="color: #1A7F8E;">"price"</span>: <span style="color: #7C3AED;">29.99</span>
|
||||
}
|
||||
],
|
||||
<span style="color: #1A7F8E;">"priority"</span>: <span style="color: #C6820E;">"HIGH"</span>
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Timeline Tab — Gantt-style processor durations</div>
|
||||
<div class="mockup-body" style="background: #fff; padding: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">content-based-routing</span>
|
||||
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">247ms total</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; cursor: pointer;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
<!-- Gantt chart -->
|
||||
<div style="padding: 10px 14px;">
|
||||
<!-- Time axis -->
|
||||
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #9C9184; margin-bottom: 4px; padding-left: 110px;">
|
||||
<span>0ms</span><span>50ms</span><span>100ms</span><span>150ms</span><span>200ms</span><span>247ms</span>
|
||||
</div>
|
||||
<!-- Processor rows -->
|
||||
<div style="display: flex; flex-direction: column; gap: 3px;">
|
||||
<!-- from:jms -->
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">from:jms</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
|
||||
<div style="position: absolute; left: 0%; width: 0.8%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
|
||||
</div>
|
||||
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">2ms</span>
|
||||
</div>
|
||||
<!-- log -->
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">log</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
|
||||
<div style="position: absolute; left: 0.8%; width: 2%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
|
||||
</div>
|
||||
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">5ms</span>
|
||||
</div>
|
||||
<!-- setHeader -->
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">setHeader</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
|
||||
<div style="position: absolute; left: 2.8%; width: 0.4%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
|
||||
</div>
|
||||
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">1ms</span>
|
||||
</div>
|
||||
<!-- bean:validate (FAILED - long) -->
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 10px; color: #C0392B; font-weight: 600; width: 100px; text-align: right; flex-shrink: 0;">bean:validate</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
|
||||
<div style="position: absolute; left: 3.2%; width: 48.6%; height: 100%; background: #C0392B; border-radius: 2px; opacity: 0.8;"></div>
|
||||
</div>
|
||||
<span style="font-size: 9px; color: #C0392B; font-weight: 500; width: 36px; flex-shrink: 0;">120ms</span>
|
||||
</div>
|
||||
<!-- to:http (skipped) -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; opacity: 0.35;">
|
||||
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">to:http</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px;"></div>
|
||||
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">—</span>
|
||||
</div>
|
||||
<!-- to:jms (skipped) -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; opacity: 0.35;">
|
||||
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">to:jms</span>
|
||||
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px;"></div>
|
||||
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 10px; color: #9C9184;">Click a bar to select that processor in the diagram</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Error Tab — grayed out when no error on selected processor</div>
|
||||
<div class="mockup-body" style="background: #fff; padding: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
|
||||
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">processor-2 · 5ms</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4; cursor: not-allowed;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
<div style="padding: 20px 14px; text-align: center; color: #9C9184; font-size: 12px;">
|
||||
No error on this processor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,207 @@
|
||||
<h2>Execution Overlay: Full Design Mockup</h2>
|
||||
<p class="subtitle">ExecutionDiagram component — diagram with execution overlay + detail panel</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">ExecutionDiagram — Failed Exchange View</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 0;">
|
||||
<!-- Top bar: Exchange summary -->
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 8px 14px; background: #fff; border-bottom: 1px solid #E4DFD8; font-size: 12px; color: #5C5347;">
|
||||
<span style="font-weight: 600; color: #1A1612;">Exchange</span>
|
||||
<code style="font-size: 11px; background: #F5F0EA; padding: 2px 6px; border-radius: 3px; color: #1A1612;">abc-123-def-456</code>
|
||||
<span style="background: #C0392B; color: white; font-size: 10px; padding: 1px 8px; border-radius: 10px; font-weight: 600;">FAILED</span>
|
||||
<span style="color: #9C9184;">sample-app / content-based-routing</span>
|
||||
<span style="color: #9C9184;">247ms</span>
|
||||
<div style="margin-left: auto; display: flex; gap: 6px;">
|
||||
<button style="font-size: 10px; padding: 3px 10px; border: 1px solid #C0392B; background: #FDF2F0; color: #C0392B; border-radius: 4px; cursor: pointer; font-weight: 500;">Jump to Error</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content: Diagram top, Detail bottom -->
|
||||
<div style="display: flex; flex-direction: column; height: 480px;">
|
||||
|
||||
<!-- TOP: Process Diagram with Overlay -->
|
||||
<div style="flex: 1; position: relative; overflow: hidden; background: #fff; border-bottom: 2px solid #E4DFD8;">
|
||||
|
||||
<!-- Breadcrumbs (if drilled down) -->
|
||||
|
||||
<!-- Diagram content -->
|
||||
<div style="padding: 24px 30px;">
|
||||
<!-- Main flow -->
|
||||
<div style="display: flex; align-items: center; gap: 0;">
|
||||
|
||||
<!-- Node: from:jms (COMPLETED) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #1A7F8E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
|
||||
</div>
|
||||
|
||||
<!-- Edge -->
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="30" y2="5" stroke="#9CA3AF" stroke-width="1.5"/><polygon points="25,2 30,5 25,8" fill="#9CA3AF"/></svg>
|
||||
|
||||
<!-- Node: log (COMPLETED) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">LOG</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
|
||||
</div>
|
||||
|
||||
<!-- Edge -->
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="30" y2="5" stroke="#9CA3AF" stroke-width="1.5"/><polygon points="25,2 30,5 25,8" fill="#9CA3AF"/></svg>
|
||||
|
||||
<!-- Node: CHOICE compound -->
|
||||
<div style="position: relative; border: 2px dashed #7C3AED; border-radius: 8px; padding: 0; background: #FAFAFF;">
|
||||
<!-- Compound header -->
|
||||
<div style="background: #7C3AED; color: white; font-size: 10px; font-weight: 600; padding: 3px 10px; border-radius: 5px 5px 0 0;">CHOICE</div>
|
||||
<div style="padding: 10px; display: flex; gap: 16px;">
|
||||
<!-- WHEN branch (taken, failed) -->
|
||||
<div style="border: 1px solid #E4DFD8; border-radius: 5px; padding: 6px; background: #fff;">
|
||||
<div style="font-size: 8px; color: #7C3AED; font-weight: 600; margin-bottom: 4px;">WHEN: header.type == 'A'</div>
|
||||
<div style="display: flex; align-items: center; gap: 0;">
|
||||
<!-- Node: bean (FAILED) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 120px; height: 48px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #C0392B;">bean:validate</div>
|
||||
<div style="font-size: 8px; color: #C0392B;">FAILED</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B; font-weight: 500;">120ms</div>
|
||||
<!-- Error icon -->
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
|
||||
</div>
|
||||
|
||||
<svg width="20" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#9CA3AF" stroke-width="1"/></svg>
|
||||
|
||||
<!-- Node: to:http (NOT EXECUTED - dimmed) -->
|
||||
<div style="position: relative; opacity: 0.35;">
|
||||
<div style="width: 120px; height: 48px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OTHERWISE branch (not taken - dimmed) -->
|
||||
<div style="border: 1px solid #E4DFD8; border-radius: 5px; padding: 6px; background: #fff; opacity: 0.35;">
|
||||
<div style="font-size: 8px; color: #7C3AED; font-weight: 600; margin-bottom: 4px;">OTHERWISE</div>
|
||||
<div style="width: 120px; height: 48px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:direct:alt</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">DIRECT</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zoom controls (bottom-right) -->
|
||||
<div style="position: absolute; bottom: 8px; right: 8px; display: flex; align-items: center; gap: 3px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px; padding: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);">
|
||||
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 12px; cursor: pointer; color: #1A1612;">+</button>
|
||||
<span style="font-size: 9px; color: #9C9184; min-width: 30px; text-align: center;">100%</span>
|
||||
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 12px; cursor: pointer; color: #1A1612;">-</button>
|
||||
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 10px; cursor: pointer; color: #1A1612;">Fit</button>
|
||||
</div>
|
||||
|
||||
<!-- Minimap (bottom-left) -->
|
||||
<div style="position: absolute; bottom: 8px; left: 8px; width: 100px; height: 60px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); overflow: hidden;">
|
||||
<div style="padding: 4px;">
|
||||
<div style="display: flex; gap: 2px; align-items: center; transform: scale(0.3); transform-origin: top left;">
|
||||
<div style="width: 60px; height: 20px; background: #1A7F8E; border-radius: 2px;"></div>
|
||||
<div style="width: 60px; height: 20px; background: #C6820E; border-radius: 2px;"></div>
|
||||
<div style="width: 100px; height: 40px; border: 1px solid #7C3AED; border-radius: 2px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SPLITTER -->
|
||||
<div style="height: 4px; background: #E4DFD8; cursor: row-resize; flex-shrink: 0;"></div>
|
||||
|
||||
<!-- BOTTOM: Detail Panel -->
|
||||
<div style="flex: 0 0 180px; background: #fff; display: flex; flex-direction: column;">
|
||||
<!-- Selected processor header -->
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
|
||||
<span style="font-size: 11px; font-weight: 600; color: #C0392B;">bean:validate</span>
|
||||
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
|
||||
<span style="font-size: 10px; color: #9C9184;">processor-5 · 120ms</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; border-bottom: 2px solid #C0392B; font-weight: 600; cursor: pointer;">Error</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.5;">Config</div>
|
||||
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab content: Error -->
|
||||
<div style="flex: 1; padding: 10px 14px; overflow-y: auto;">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Exception</div>
|
||||
<div style="font-size: 12px; color: #C0392B; font-weight: 500;">javax.validation.ValidationException</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Message</div>
|
||||
<div style="font-size: 12px; color: #1A1612;">Order quantity must be positive: received -3</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Root Cause</div>
|
||||
<div style="font-size: 12px; color: #C0392B;">java.lang.IllegalArgumentException: quantity must be > 0</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Stack Trace</div>
|
||||
<pre style="font-size: 10px; color: #5C5347; background: #F5F0EA; padding: 8px; border-radius: 4px; overflow-x: auto; margin: 0; line-height: 1.6;">at com.example.OrderValidator.validate(OrderValidator.java:42)
|
||||
at com.example.OrderRoute.process(OrderRoute.java:18)
|
||||
at org.apache.camel.processor.DelegateSyncProcessor.process(...)
|
||||
at org.apache.camel.processor.Pipeline.process(Pipeline.java:184)
|
||||
... 8 more</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<h3>Design Decisions Shown</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px; color: #5C5347;">
|
||||
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #3D7C47;">
|
||||
<strong style="color: #1A1612;">Executed (OK)</strong><br/>
|
||||
Green left border, duration badge bottom-right
|
||||
</div>
|
||||
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #C0392B;">
|
||||
<strong style="color: #1A1612;">Failed</strong><br/>
|
||||
Red border, red tint background, red ! badge top-right
|
||||
</div>
|
||||
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #9C9184;">
|
||||
<strong style="color: #1A1612;">Not Executed</strong><br/>
|
||||
Dimmed to 35% opacity — full topology visible
|
||||
</div>
|
||||
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #C6820E;">
|
||||
<strong style="color: #1A1612;">Selected</strong><br/>
|
||||
Amber ring (existing), detail panel updates below
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
143
.superpowers/brainstorm/14618-1774629192/iteration-stepper.html
Normal file
143
.superpowers/brainstorm/14618-1774629192/iteration-stepper.html
Normal file
@@ -0,0 +1,143 @@
|
||||
<h2>Per-Compound Iteration Stepper</h2>
|
||||
<p class="subtitle">Each loop/split compound gets its own stepper in the header bar</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Loop with iteration stepper — iteration 3 of 5</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
|
||||
|
||||
<!-- LOOP compound -->
|
||||
<div style="position: relative; border: 2px dashed #7C3AED; border-radius: 8px; background: #FAFAFF; max-width: 600px;">
|
||||
<!-- Compound header with stepper -->
|
||||
<div style="background: #7C3AED; color: white; font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 5px 5px 0 0; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>LOOP</span>
|
||||
<!-- Iteration stepper -->
|
||||
<div style="display: flex; align-items: center; gap: 4px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px;">
|
||||
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">‹</button>
|
||||
<span style="font-size: 10px; min-width: 30px; text-align: center; font-variant-numeric: tabular-nums;">3 / 5</span>
|
||||
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loop body: showing iteration 3 data -->
|
||||
<div style="padding: 12px; display: flex; align-items: center; gap: 0;">
|
||||
<!-- transform (OK in iteration 3) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 4px; background: #C6820E;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">transform</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">TRANSFORM</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">3ms</div>
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="24" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="17,2 22,5 17,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- to:http (OK in iteration 3) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 4px; background: #3D7C47;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">45ms</div>
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="24" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="17,2 22,5 17,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- log (OK in iteration 3) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 4px; background: #C6820E;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">log:result</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">LOG</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">1ms</div>
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">✓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">Nested Loops</h3>
|
||||
<p class="subtitle">Each compound level has its own independent stepper</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Outer loop (iteration 2/3) containing inner split (branch 1/4)</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
|
||||
|
||||
<!-- Outer LOOP -->
|
||||
<div style="border: 2px dashed #7C3AED; border-radius: 8px; background: #FAFAFF; max-width: 550px;">
|
||||
<div style="background: #7C3AED; color: white; font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 5px 5px 0 0; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>LOOP</span>
|
||||
<div style="display: flex; align-items: center; gap: 4px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px;">
|
||||
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">‹</button>
|
||||
<span style="font-size: 10px; min-width: 30px; text-align: center;">2 / 3</span>
|
||||
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 0;">
|
||||
|
||||
<!-- Processor before split -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 110px; height: 44px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 4px; background: #C6820E;"></div>
|
||||
<div style="padding: 3px 6px;">
|
||||
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">setBody</div>
|
||||
<div style="font-size: 8px; color: #9C9184;">SET_BODY</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="20" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="16" y2="5" stroke="#3D7C47" stroke-width="1.5"/></svg>
|
||||
|
||||
<!-- Inner SPLIT -->
|
||||
<div style="border: 2px dashed #7C3AED; border-radius: 6px; background: #F8F7FF;">
|
||||
<div style="background: #9B6AED; color: white; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 3px 3px 0 0; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>SPLIT</span>
|
||||
<div style="display: flex; align-items: center; gap: 3px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 3px;">
|
||||
<button style="width: 16px; height: 16px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 9px; display: flex; align-items: center; justify-content: center;">‹</button>
|
||||
<span style="font-size: 9px; min-width: 26px; text-align: center;">1 / 4</span>
|
||||
<button style="width: 16px; height: 16px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 9px; display: flex; align-items: center; justify-content: center;">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 8px; display: flex; align-items: center; gap: 0;">
|
||||
<div style="position: relative;">
|
||||
<div style="width: 100px; height: 40px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 4px; border-left: 3px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 3px; background: #3D7C47;"></div>
|
||||
<div style="padding: 2px 5px;">
|
||||
<div style="font-size: 8px; font-weight: 600; color: #1A1612;">to:kafka</div>
|
||||
<div style="font-size: 7px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; top: -4px; right: -4px; width: 12px; height: 12px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white;">✓</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">Stepper Behavior</h3>
|
||||
<div style="font-size: 13px; color: #5C5347; line-height: 1.8;">
|
||||
<ul style="margin: 0; padding-left: 20px;">
|
||||
<li><strong>Independent per compound</strong> — outer loop at iteration 2, inner split at branch 1</li>
|
||||
<li><strong>Overlay updates per-compound</strong> — stepping the loop re-renders its children's execution data for that iteration</li>
|
||||
<li><strong>CHOICE shows which branch was taken</strong> — no stepper, just highlights the taken branch</li>
|
||||
<li><strong>Keyboard</strong> — when a compound is focused/hovered, left/right arrow keys step through iterations</li>
|
||||
<li><strong>Detail panel syncs</strong> — selecting a processor inside a loop shows that iteration's data</li>
|
||||
</ul>
|
||||
</div>
|
||||
166
.superpowers/brainstorm/14618-1774629192/layout-overview.html
Normal file
166
.superpowers/brainstorm/14618-1774629192/layout-overview.html
Normal file
@@ -0,0 +1,166 @@
|
||||
<h2>Execution Overlay: Page Layout</h2>
|
||||
<p class="subtitle">How should the diagram + execution details be arranged?</p>
|
||||
|
||||
<div class="cards">
|
||||
<!-- Option A: Horizontal Split -->
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
|
||||
<!-- IDE-style: diagram top, detail bottom -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; height: 280px;">
|
||||
<!-- Top: Diagram -->
|
||||
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
|
||||
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
|
||||
<!-- Mini route flow mockup -->
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-left: 20px;">
|
||||
<div style="width: 60px; height: 28px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">from:jms</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #3D7C47;">log</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #C0392B; opacity: 0.9;">bean</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #3D7C47; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; opacity: 0.4;">to:http</div>
|
||||
</div>
|
||||
<!-- Zoom controls hint -->
|
||||
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
|
||||
<!-- Iteration stepper -->
|
||||
<div style="position: absolute; top: 6px; right: 6px; font-size: 8px; color: #C6820E; background: #2a2520; padding: 2px 8px; border: 1px solid #3a3530; border-radius: 3px;">Loop 2/5</div>
|
||||
</div>
|
||||
<!-- Resizable splitter -->
|
||||
<div style="height: 3px; background: #3a3530; border-radius: 2px; cursor: row-resize;"></div>
|
||||
<!-- Bottom: Details -->
|
||||
<div style="flex: 0 0 100px; background: #2a2520; border-radius: 4px; padding: 8px; overflow: hidden;">
|
||||
<div style="display: flex; gap: 12px; font-size: 9px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 4px; margin-bottom: 6px;">
|
||||
<span style="color: #C6820E; border-bottom: 2px solid #C6820E; padding-bottom: 3px;">Input</span>
|
||||
<span>Output</span>
|
||||
<span>Headers</span>
|
||||
<span>Error</span>
|
||||
<span>Timeline</span>
|
||||
</div>
|
||||
<div style="font-family: monospace; font-size: 8px; color: #9C9184; line-height: 1.5;">
|
||||
<div>{"orderId": "ORD-1234",</div>
|
||||
<div> "product": "Widget A",</div>
|
||||
<div> "quantity": 5}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A: Top/Bottom Split (IDE Style)</h3>
|
||||
<p>Diagram on top, tabbed detail panel below. Resizable splitter between them. Maximizes diagram width. Tabs: Input, Output, Headers, Error, Timeline.</p>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Full diagram width</li><li>Familiar IDE pattern</li><li>Detail panel always visible</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Vertical space shared</li><li>Diagram shrinks on small screens</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option B: Right Panel -->
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
|
||||
<div style="display: flex; gap: 8px; height: 280px;">
|
||||
<!-- Left: Diagram -->
|
||||
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
|
||||
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-left: 10px;">
|
||||
<div style="width: 55px; height: 26px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white;">from:jms</div>
|
||||
<div style="width: 14px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 55px; height: 26px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white; border: 2px solid #3D7C47;">log</div>
|
||||
<div style="width: 14px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 55px; height: 26px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white; border: 2px solid #C0392B;">bean</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
|
||||
</div>
|
||||
<!-- Resizable splitter -->
|
||||
<div style="width: 3px; background: #3a3530; border-radius: 2px; cursor: col-resize;"></div>
|
||||
<!-- Right: Detail Panel -->
|
||||
<div style="flex: 0 0 200px; background: #2a2520; border-radius: 4px; padding: 8px; overflow: hidden;">
|
||||
<div style="font-size: 9px; color: #C6820E; font-weight: 600; margin-bottom: 6px;">log (processor-3)</div>
|
||||
<div style="font-size: 8px; color: #3D7C47; margin-bottom: 8px;">COMPLETED - 12ms</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 2px; font-size: 8px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 4px; margin-bottom: 6px;">
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<span style="color: #C6820E; font-weight: 600;">Input</span>
|
||||
<span>Output</span>
|
||||
<span>Headers</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-family: monospace; font-size: 7px; color: #9C9184; line-height: 1.4;">
|
||||
<div>{"orderId": "ORD-1234",</div>
|
||||
<div> "product": "Widget A",</div>
|
||||
<div> "quantity": 5,</div>
|
||||
<div> "price": 29.99}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B: Left/Right Split</h3>
|
||||
<p>Diagram on left, collapsible detail panel on right. Slide-in when node selected. Diagram keeps full height.</p>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Full diagram height</li><li>Panel can collapse</li><li>Good for wide screens</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Steals diagram width</li><li>Tight on narrow screens</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Option C: Hybrid -->
|
||||
<div class="card" data-choice="c" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; height: 280px;">
|
||||
<!-- Top: Full width diagram -->
|
||||
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
|
||||
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-left: 20px;">
|
||||
<div style="width: 60px; height: 28px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">from:jms</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #3D7C47;">log</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #C0392B;">bean</div>
|
||||
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
|
||||
<div style="width: 60px; height: 28px; background: #3D7C47; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; opacity: 0.4;">to:http</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
|
||||
</div>
|
||||
<!-- Bottom: Two-column detail -->
|
||||
<div style="height: 3px; background: #3a3530; border-radius: 2px;"></div>
|
||||
<div style="flex: 0 0 100px; display: flex; gap: 8px;">
|
||||
<!-- Left: Processor list / timeline -->
|
||||
<div style="flex: 0 0 140px; background: #2a2520; border-radius: 4px; padding: 6px; overflow: hidden;">
|
||||
<div style="font-size: 8px; color: #9C9184; margin-bottom: 4px; font-weight: 600;">Processors</div>
|
||||
<div style="font-size: 7px; line-height: 1.8;">
|
||||
<div style="color: #3D7C47; padding: 1px 4px; background: #2a2a20; border-radius: 2px;">from:jms - 2ms</div>
|
||||
<div style="color: #C6820E; padding: 1px 4px; background: #3a3020; border-radius: 2px; border-left: 2px solid #C6820E;">log - 12ms</div>
|
||||
<div style="color: #C0392B; padding: 1px 4px;">bean - FAILED</div>
|
||||
<div style="color: #5C5347; padding: 1px 4px; opacity: 0.5;">to:http - skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Selected processor detail -->
|
||||
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 6px; overflow: hidden;">
|
||||
<div style="display: flex; gap: 8px; font-size: 8px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 3px; margin-bottom: 4px;">
|
||||
<span style="color: #C6820E;">Input</span>
|
||||
<span>Output</span>
|
||||
<span>Headers</span>
|
||||
</div>
|
||||
<div style="font-family: monospace; font-size: 7px; color: #9C9184; line-height: 1.4;">
|
||||
<div>{"orderId": "ORD-1234",</div>
|
||||
<div> "product": "Widget A"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>C: Top/Bottom with Processor List</h3>
|
||||
<p>Diagram on top, bottom split into processor list (left) + detail tabs (right). Clicking processor in list or diagram syncs selection. Most information density.</p>
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Processor list as navigation</li><li>Full diagram width</li><li>Maximum information density</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>More complex layout</li><li>May feel crowded</li></ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
190
.superpowers/brainstorm/14618-1774629192/overlay-intensity.html
Normal file
190
.superpowers/brainstorm/14618-1774629192/overlay-intensity.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<h2>Execution Overlay: Visual Intensity Comparison</h2>
|
||||
<p class="subtitle">How strong should the overlay tinting be?</p>
|
||||
|
||||
<div class="split">
|
||||
<!-- Current: Subtle -->
|
||||
<div class="mockup" data-choice="subtle" onclick="toggleSelect(this)">
|
||||
<div class="mockup-header">Current: Subtle (border only)</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 16px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<!-- OK node - border only -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Completed</span>
|
||||
<div style="position: relative; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #1A7F8E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Failed node - border only -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Failed</span>
|
||||
<div style="position: relative; width: 160px; height: 52px; background: #fff; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">bean:validate</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">BEAN</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Skipped node -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Skipped</span>
|
||||
<div style="opacity: 0.35; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proposed: Tinted backgrounds -->
|
||||
<div class="mockup" data-choice="tinted" onclick="toggleSelect(this)">
|
||||
<div class="mockup-header">Proposed: Tinted backgrounds</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 16px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<!-- OK node - green tint -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Completed</span>
|
||||
<div style="position: relative; width: 160px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #1A7F8E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Failed node - red tint -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Failed</span>
|
||||
<div style="position: relative; width: 160px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
|
||||
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Skipped node -->
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 10px; color: #9C9184; width: 70px;">Skipped</span>
|
||||
<div style="opacity: 0.35; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">Full Flow Comparison</h3>
|
||||
<p class="subtitle">Same route, tinted version — see how it reads at a glance</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Tinted overlay on a full route</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 0;">
|
||||
|
||||
<!-- from:jms (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #1A7F8E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- log (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">LOG</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- setHeader (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">setHeader:type</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">SET_HEADER</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">1ms</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- bean:validate (FAILED) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
|
||||
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
|
||||
|
||||
<!-- to:http (SKIPPED) -->
|
||||
<div style="opacity: 0.35;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
|
||||
|
||||
<!-- to:jms (SKIPPED) -->
|
||||
<div style="opacity: 0.35;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:jms:result</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; font-size: 11px; color: #5C5347;">
|
||||
<strong>Note:</strong> Edges between executed nodes turn green. Edges leading to skipped nodes become dashed gray.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,159 @@
|
||||
<h2>Execution Overlay: Success + Error Markers</h2>
|
||||
<p class="subtitle">Every executed node gets a status badge — green check or red exclamation</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Full route with status markers</div>
|
||||
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 0;">
|
||||
|
||||
<!-- from:jms (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #1A7F8E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
|
||||
<!-- Success marker -->
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- log (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">LOG</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- setHeader (OK) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">setHeader:type</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">SET_HEADER</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">1ms</div>
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">✓</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
|
||||
|
||||
<!-- bean:validate (FAILED) -->
|
||||
<div style="position: relative;">
|
||||
<div style="width: 140px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #C6820E;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
|
||||
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
|
||||
<!-- Error marker -->
|
||||
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
|
||||
|
||||
<!-- to:http (SKIPPED) -->
|
||||
<div style="opacity: 0.35;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
|
||||
|
||||
<!-- to:jms (SKIPPED) -->
|
||||
<div style="opacity: 0.35;">
|
||||
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
|
||||
<div style="height: 5px; background: #3D7C47;"></div>
|
||||
<div style="padding: 4px 8px;">
|
||||
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:jms:result</div>
|
||||
<div style="font-size: 9px; color: #9C9184;">TO</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">Node State Legend</h3>
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
|
||||
|
||||
<!-- Completed -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<div style="position: relative; width: 80px; height: 36px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 4px; border-left: 3px solid #3D7C47;">
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">✓</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">5ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600; color: #3D7C47;">Completed</div>
|
||||
<div style="font-size: 10px; color: #9C9184;">Green tint + border + check badge + duration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<div style="position: relative; width: 80px; height: 36px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 4px;">
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; font-weight: bold;">!</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B;">120ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600; color: #C0392B;">Failed</div>
|
||||
<div style="font-size: 10px; color: #9C9184;">Red tint + border + ! badge + duration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-route failure -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<div style="position: relative; width: 80px; height: 36px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 4px;">
|
||||
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; font-weight: bold;">!</div>
|
||||
<div style="position: absolute; bottom: 1px; left: 4px; font-size: 8px; color: #C0392B;">↴</div>
|
||||
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B;">85ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600; color: #C0392B;">Sub-route Failure</div>
|
||||
<div style="font-size: 10px; color: #9C9184;">Same as failed + drill-down arrow</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skipped -->
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<div style="opacity: 0.35; width: 80px; height: 36px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px;">
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; font-weight: 600; color: #9C9184;">Skipped</div>
|
||||
<div style="font-size: 10px; color: #9C9184;">35% opacity, no badge, no duration</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 24px;">Edge States</h3>
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<svg width="60" height="10"><line x1="0" y1="5" x2="50" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="47,2 53,5 47,8" fill="#3D7C47"/></svg>
|
||||
<div style="font-size: 11px; color: #5C5347;"><strong>Traversed</strong> — green, solid</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
|
||||
<svg width="60" height="10"><line x1="0" y1="5" x2="50" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/><polygon points="47,2 53,5 47,8" fill="#9CA3AF"/></svg>
|
||||
<div style="font-size: 11px; color: #5C5347;"><strong>Not traversed</strong> — gray, dashed</div>
|
||||
</div>
|
||||
</div>
|
||||
3
.superpowers/brainstorm/14618-1774629192/waiting.html
Normal file
3
.superpowers/brainstorm/14618-1774629192/waiting.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -0,0 +1,181 @@
|
||||
<h2>AppConfigDetailPage — New Sections</h2>
|
||||
<p class="subtitle">Taps overview, route recording map, and compress success toggle added to existing config page</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">AppConfigDetailPage — Full Layout (scrollable)</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<!-- Back + Header -->
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:16px;">←</span>
|
||||
<span style="font-size:16px;font-weight:600;">order-service</span>
|
||||
<span style="font-family:monospace;font-size:11px;color:#6b7280;margin-left:8px;">v14 · Updated 3 min ago</span>
|
||||
<div style="margin-left:auto;display:flex;gap:8px;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">✎</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EXISTING: Logging Section ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Logging</div>
|
||||
<div style="display:flex;gap:24px;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Log Forwarding Level</div>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">INFO</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EXISTING: Observability Section ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Observability</div>
|
||||
<div style="display:flex;gap:24px;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Engine Level</div>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">REGULAR</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Payload Capture</div>
|
||||
<span style="background:#2d1f3b;color:#d8b4fe;padding:2px 10px;border-radius:4px;font-size:11px;">BOTH</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Metrics</div>
|
||||
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:4px;font-size:11px;">ON</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Sampling Rate</div>
|
||||
<span style="font-family:monospace;font-size:12px;color:#e0e0e0;">1.0</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Compress Success</div>
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:4px;font-size:11px;">OFF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EXISTING: Traced Processors ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traced Processors</div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:8px;">2 processors with custom capture modes</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor ID</th>
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture Mode</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">unmarshal1</td>
|
||||
<td style="padding:6px 8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">toDatabase</td>
|
||||
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NEW: Data Extraction Taps ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Data Extraction Taps</div>
|
||||
<span style="font-size:11px;color:#6b7280;">3 taps · manage on route pages</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Attribute</th>
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Expression</th>
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Language</th>
|
||||
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Enabled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-weight:500;">orderId</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">${body.orderId}</span></td>
|
||||
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:4px;font-size:10px;">simple</span></td>
|
||||
<td style="padding:6px 8px;text-align:center;"><span style="color:#4ade80;">✓</span></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-weight:500;">customerId</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">${body.customer.id}</span></td>
|
||||
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:4px;font-size:10px;">simple</span></td>
|
||||
<td style="padding:6px 8px;text-align:center;"><span style="color:#4ade80;">✓</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 8px;font-weight:500;">orderTotal</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">$.total</span></td>
|
||||
<td style="padding:6px 8px;"><span style="background:#3b2f1f;color:#fcd34d;padding:1px 6px;border-radius:4px;font-size:10px;">jsonpath</span></td>
|
||||
<td style="padding:6px 8px;text-align:center;"><span style="color:#6b7280;">✗</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NEW: Route Recording ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Route Recording</div>
|
||||
<span style="font-size:11px;color:#6b7280;">4 of 5 routes recording</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
|
||||
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Recording</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processOrder</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processPayment</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">sendNotification</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">handleRefund</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">healthCheck</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
<h2>AppConfigDetailPage — Final Layout</h2>
|
||||
<p class="subtitle">Three clean sections: Settings, Traces & Taps, Route Recording</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">AppConfigDetailPage — Complete</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<!-- Back + Header -->
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:16px;">←</span>
|
||||
<span style="font-size:16px;font-weight:600;">order-service</span>
|
||||
<span style="font-family:monospace;font-size:11px;color:#6b7280;margin-left:8px;">v14 · Updated 3 min ago</span>
|
||||
<div style="margin-left:auto;display:flex;gap:8px;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">✎</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 1: Settings ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:12px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Settings</div>
|
||||
<div style="display:flex;gap:28px;flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Log Forwarding</div>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">INFO</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Engine Level</div>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">REGULAR</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Payload Capture</div>
|
||||
<span style="background:#2d1f3b;color:#d8b4fe;padding:2px 10px;border-radius:4px;font-size:11px;">BOTH</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Metrics</div>
|
||||
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:4px;font-size:11px;">ON</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Sampling Rate</div>
|
||||
<span style="font-family:monospace;font-size:12px;color:#e0e0e0;">1.0</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Compress Success</div>
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:4px;font-size:11px;">OFF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 2: Traces & Taps ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
|
||||
<span style="font-size:11px;color:#6b7280;">2 traced · 3 taps · manage taps on route pages</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
|
||||
<td style="padding:8px;">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">toDatabase</td>
|
||||
<td style="padding:8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
|
||||
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;">—</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
|
||||
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;">—</span></td>
|
||||
<td style="padding:8px;">
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">✗</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ═══ Section 3: Route Recording ═══ -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Route Recording</div>
|
||||
<span style="font-size:11px;color:#6b7280;">4 of 5 routes recording</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
|
||||
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;width:80px;">Recording</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processOrder</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processPayment</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">sendNotification</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">handleRefund</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">healthCheck</td>
|
||||
<td style="padding:6px 8px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<h2>AppConfigDetailPage — Merged "Traces & Taps" Section</h2>
|
||||
<p class="subtitle">Single table combining traced processors and data extraction taps</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Traces & Taps — Merged Table</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
|
||||
<span style="font-size:11px;color:#6b7280;">2 traced · 3 taps · manage taps on route pages</span>
|
||||
</div>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Processor with both trace + taps -->
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:8px;">
|
||||
<span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span>
|
||||
</td>
|
||||
<td style="padding:8px;">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Processor with trace only -->
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">toDatabase</td>
|
||||
<td style="padding:8px;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span>
|
||||
</td>
|
||||
<td style="padding:8px;">
|
||||
<span style="color:#6b7280;font-size:11px;">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Processor with tap only (no trace override) -->
|
||||
<tr>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
|
||||
<td style="padding:8px;">
|
||||
<span style="color:#6b7280;font-size:11px;">—</span>
|
||||
</td>
|
||||
<td style="padding:8px;">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">✗</span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px;"></div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Design Notes</h3>
|
||||
<ul style="font-size:14px;line-height:1.8;">
|
||||
<li><strong>One row per processor</strong> that has either a capture override or taps (or both)</li>
|
||||
<li><strong>Capture column:</strong> shows the trace capture mode badge, or em-dash if default</li>
|
||||
<li><strong>Taps column:</strong> attribute name badges with enabled/disabled indicator (✓ / ✗), or em-dash if none</li>
|
||||
<li><strong>Tap badges color-coded by language:</strong> blue = simple, yellow = jsonpath (matches RouteDetail tap table)</li>
|
||||
<li><strong>Edit mode:</strong> capture column becomes a dropdown, taps remain read-only (manage on route pages)</li>
|
||||
<li><strong>Empty state:</strong> "No processor-specific traces or taps configured" with link to route pages</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<h2>ExchangeDetail — Business Attributes & Replay</h2>
|
||||
<p class="subtitle">New elements added to the existing exchange detail page</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Exchange Detail Page — Header Card (enhanced)</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<!-- Exchange Header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;">
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<span style="width:10px;height:10px;border-radius:50%;background:#4ade80;display:inline-block;"></span>
|
||||
<span style="font-family:monospace;font-size:15px;font-weight:600;">a1b2c3d4-e5f6-7890-abcd-ef1234567890</span>
|
||||
<span style="background:#065f46;color:#6ee7b7;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">COMPLETED</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;font-size:12px;color:#9ca3af;">
|
||||
<span>Route: <span style="color:#60a5fa;">processOrder</span></span>
|
||||
<span>App: <span style="font-family:monospace;">order-service</span></span>
|
||||
<span>Correlation: <span style="font-family:monospace;">corr-abc123</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<!-- REPLAY BUTTON (NEW) -->
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:6px;">
|
||||
↻ Replay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Attributes Strip (NEW) -->
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;margin-bottom:16px;">
|
||||
<span style="font-size:11px;color:#9ca3af;margin-right:4px;line-height:24px;">Attributes</span>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">orderId: ORD-2024-78542</span>
|
||||
<span style="background:#3b1f4b;color:#d8b4fe;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">customerId: CUST-1234</span>
|
||||
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">orderTotal: €149.90</span>
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">region: EU-WEST</span>
|
||||
</div>
|
||||
|
||||
<!-- Stat boxes row -->
|
||||
<div style="display:flex;gap:12px;">
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
|
||||
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Duration</div>
|
||||
<div style="font-size:18px;font-weight:600;color:#4ade80;">245ms</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
|
||||
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Agent</div>
|
||||
<div style="font-size:14px;font-family:monospace;color:#e0e0e0;">order-svc-01</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
|
||||
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Started</div>
|
||||
<div style="font-size:14px;font-family:monospace;color:#e0e0e0;">14:23:45.123</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
|
||||
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Processors</div>
|
||||
<div style="font-size:18px;font-weight:600;color:#e0e0e0;">12</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Replay Confirmation Dialog</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:24px;width:480px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;">✕</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:#9ca3af;margin-bottom:16px;">
|
||||
This will re-execute the exchange on the target agent. The original exchange data will be used as input.
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Original Exchange</div>
|
||||
<div style="font-family:monospace;font-size:12px;background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;">a1b2c3d4-e5f6-7890-abcd-ef1234567890</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Target Agent</div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:monospace;">order-svc-01</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Route</div>
|
||||
<div style="font-family:monospace;font-size:12px;background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;">processOrder</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">↻ Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Dashboard — Exchanges Table (with business attributes)</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:16px;font-family:system-ui,-apple-system,sans-serif;font-size:12px;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;text-align:left;">
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Status</th>
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">App</th>
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Attributes</th>
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Exchange ID</th>
|
||||
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #1e1e3a;">
|
||||
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#4ade80;display:inline-block;"></span> <span style="color:#6ee7b7;font-size:11px;">OK</span></td>
|
||||
<td style="padding:8px 12px;color:#60a5fa;">processOrder</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;">order-svc</td>
|
||||
<td style="padding:8px 12px;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">ORD-78542</span>
|
||||
<span style="background:#3b1f4b;color:#d8b4fe;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">CUST-1234</span>
|
||||
</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">a1b2c3d4-e5f6…</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;color:#4ade80;">245ms</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #1e1e3a;">
|
||||
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#f87171;display:inline-block;"></span> <span style="color:#fca5a5;font-size:11px;">ERR</span></td>
|
||||
<td style="padding:8px 12px;color:#60a5fa;">processPayment</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;">payment-svc</td>
|
||||
<td style="padding:8px 12px;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">PAY-91023</span>
|
||||
<span style="color:#6b7280;font-size:10px;">+2</span>
|
||||
</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">f8e7d6c5-b4a3…</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;color:#f87171;">1,234ms</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #1e1e3a;">
|
||||
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#4ade80;display:inline-block;"></span> <span style="color:#6ee7b7;font-size:11px;">OK</span></td>
|
||||
<td style="padding:8px 12px;color:#60a5fa;">sendNotification</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;">notif-svc</td>
|
||||
<td style="padding:8px 12px;"><span style="color:#6b7280;font-size:10px;font-style:italic;">—</span></td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">12345678-abcd…</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;color:#4ade80;">89ms</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px;font-size:11px;color:#6b7280;">
|
||||
Note: Attributes column shows first 2 values as compact badges, "+N" overflow indicator when more exist. Em-dash when no attributes extracted.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,138 @@
|
||||
<h2>Replay Dialog — Revised</h2>
|
||||
<p class="subtitle">Target agent selection + editable payload and headers</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Replay Exchange Dialog (large modal)</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:640px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
|
||||
|
||||
<!-- Dialog header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:18px;">✕</span>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px;">
|
||||
<!-- Warning -->
|
||||
<div style="font-size:12px;color:#fbbf24;background:#3b2f1f;border:1px solid #854d0e;border-radius:6px;padding:8px 12px;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
|
||||
<span>⚠</span> This will re-execute the exchange on the selected agent.
|
||||
</div>
|
||||
|
||||
<!-- Target Agent -->
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target Agent</div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:monospace;">order-svc-01</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#6b7280;margin-top:4px;">Only LIVE agents for this application are shown</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Headers / Body -->
|
||||
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
|
||||
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Headers</div>
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Body</div>
|
||||
</div>
|
||||
|
||||
<!-- Headers tab content -->
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;margin-bottom:16px;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:11px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:4px 8px;color:#9ca3af;font-weight:500;width:35%;">Key</th>
|
||||
<th style="text-align:left;padding:4px 8px;color:#9ca3af;font-weight:500;">Value</th>
|
||||
<th style="width:32px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #1e1e3a;">
|
||||
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="Content-Type" /></td>
|
||||
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="application/json" /></td>
|
||||
<td style="padding:4px 8px;text-align:center;"><span style="color:#f87171;cursor:pointer;font-size:14px;">✕</span></td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #1e1e3a;">
|
||||
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="X-Correlation-Id" /></td>
|
||||
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="corr-abc123" /></td>
|
||||
<td style="padding:4px 8px;text-align:center;"><span style="color:#f87171;cursor:pointer;font-size:14px;">✕</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" style="padding:6px 8px;">
|
||||
<span style="color:#3b82f6;cursor:pointer;font-size:11px;">+ Add header</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">↻ Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Replay Dialog — Body Tab</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:640px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
|
||||
|
||||
<!-- Dialog header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:18px;">✕</span>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px;">
|
||||
<!-- Warning -->
|
||||
<div style="font-size:12px;color:#fbbf24;background:#3b2f1f;border:1px solid #854d0e;border-radius:6px;padding:8px 12px;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
|
||||
<span>⚠</span> This will re-execute the exchange on the selected agent.
|
||||
</div>
|
||||
|
||||
<!-- Target Agent (collapsed) -->
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target Agent</div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:monospace;">order-svc-01</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Headers / Body -->
|
||||
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Headers</div>
|
||||
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Body</div>
|
||||
</div>
|
||||
|
||||
<!-- Body tab content — editable code area -->
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:0;margin-bottom:16px;position:relative;">
|
||||
<div style="display:flex;justify-content:flex-end;padding:6px 8px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:10px;color:#6b7280;background:#1a1a2e;padding:2px 8px;border-radius:4px;">JSON</span>
|
||||
</div>
|
||||
<pre style="margin:0;padding:12px;font-family:monospace;font-size:11px;line-height:1.6;color:#e0e0e0;min-height:160px;overflow:auto;white-space:pre;"><span style="color:#9ca3af;">{</span>
|
||||
<span style="color:#7dd3fc;">"orderId"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"ORD-2024-78542"</span><span style="color:#9ca3af;">,</span>
|
||||
<span style="color:#7dd3fc;">"customerId"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"CUST-1234"</span><span style="color:#9ca3af;">,</span>
|
||||
<span style="color:#7dd3fc;">"items"</span><span style="color:#9ca3af;">:</span> <span style="color:#9ca3af;">[</span>
|
||||
<span style="color:#9ca3af;">{</span>
|
||||
<span style="color:#7dd3fc;">"sku"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"WIDGET-001"</span><span style="color:#9ca3af;">,</span>
|
||||
<span style="color:#7dd3fc;">"qty"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">3</span><span style="color:#9ca3af;">,</span>
|
||||
<span style="color:#7dd3fc;">"price"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">49.97</span>
|
||||
<span style="color:#9ca3af;">}</span>
|
||||
<span style="color:#9ca3af;">],</span>
|
||||
<span style="color:#7dd3fc;">"total"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">149.90</span>
|
||||
<span style="color:#9ca3af;">}</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">↻ Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,221 @@
|
||||
<h2>RouteDetail — Tap Management & Recording Toggle</h2>
|
||||
<p class="subtitle">New "Taps" tab on RouteDetail + recording toggle in header</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">RouteDetail Page — Header with Recording Toggle</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<!-- Route header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;">
|
||||
<div>
|
||||
<div style="font-size:16px;font-weight:600;margin-bottom:4px;">processOrder</div>
|
||||
<div style="font-size:12px;color:#9ca3af;">
|
||||
<span style="font-family:monospace;">order-service</span>
|
||||
<span style="margin:0 8px;color:#2d2d50;">|</span>
|
||||
<span style="color:#4ade80;">99.2% success</span>
|
||||
<span style="margin:0 8px;color:#2d2d50;">|</span>
|
||||
<span>245ms avg</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<!-- Recording toggle -->
|
||||
<div style="display:flex;align-items:center;gap:8px;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:6px 12px;">
|
||||
<span style="font-size:11px;color:#9ca3af;">Recording</span>
|
||||
<div style="width:36px;height:20px;background:#3b82f6;border-radius:10px;position:relative;cursor:pointer;">
|
||||
<div style="width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;transition:all 0.2s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI strip (abbreviated) -->
|
||||
<div style="display:flex;gap:10px;margin-bottom:16px;">
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#9ca3af;">Success Rate</div>
|
||||
<div style="font-size:16px;font-weight:600;color:#4ade80;">99.2%</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#9ca3af;">Avg Duration</div>
|
||||
<div style="font-size:16px;font-weight:600;">245ms</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#9ca3af;">Total</div>
|
||||
<div style="font-size:16px;font-weight:600;">12,482</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#9ca3af;">Active Taps</div>
|
||||
<div style="font-size:16px;font-weight:600;color:#60a5fa;">3</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div style="display:flex;gap:0;border-bottom:1px solid #2d2d50;margin-bottom:16px;">
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Overview</div>
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Processors</div>
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Errors</div>
|
||||
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Executions</div>
|
||||
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Taps</div>
|
||||
</div>
|
||||
|
||||
<!-- Taps tab content -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<div style="font-size:13px;font-weight:600;">Data Extraction Taps</div>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;display:flex;align-items:center;gap:4px;">+ Add Tap</button>
|
||||
</div>
|
||||
|
||||
<!-- Taps table -->
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;overflow:hidden;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Attribute</th>
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Expression</th>
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Language</th>
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Target</th>
|
||||
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Type</th>
|
||||
<th style="text-align:center;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Enabled</th>
|
||||
<th style="width:60px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px 12px;font-weight:500;">orderId</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">${body.orderId}</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">simple</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">BUSINESS</span></td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">✎</span>
|
||||
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">✕</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px 12px;font-weight:500;">customerId</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">${body.customer.id}</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">simple</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">CORRELATION</span></td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">✎</span>
|
||||
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">✕</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 12px;font-weight:500;">orderTotal</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
|
||||
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">$.total</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:4px;font-size:10px;">jsonpath</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
|
||||
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">BUSINESS</span></td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
|
||||
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">✎</span>
|
||||
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">✕</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Add/Edit Tap — Modal Dialog</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:520px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:15px;font-weight:600;">Add Tap</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:18px;">✕</span>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px;">
|
||||
<!-- Attribute Name -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Name <span style="color:#f87171;">*</span></div>
|
||||
<input style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-size:12px;box-sizing:border-box;" placeholder="e.g. orderId, customerId" />
|
||||
</div>
|
||||
|
||||
<!-- Processor -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Processor <span style="color:#f87171;">*</span></div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="color:#6b7280;">Select processor…</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#6b7280;margin-top:3px;">Processors from this route's diagram</div>
|
||||
</div>
|
||||
|
||||
<!-- Two columns: Language + Target -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:14px;">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Language <span style="color:#f87171;">*</span></div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>simple</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target <span style="color:#f87171;">*</span></div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>OUTPUT</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expression -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Expression <span style="color:#f87171;">*</span></div>
|
||||
<textarea style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical;min-height:48px;" placeholder="e.g. ${body.orderId} or $.customer.id">${body.orderId}</textarea>
|
||||
<div style="font-size:10px;color:#6b7280;margin-top:3px;">Camel expression — evaluated at the selected processor</div>
|
||||
</div>
|
||||
|
||||
<!-- Attribute Type -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Type</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div style="background:#1e3a5f;color:#7dd3fc;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #3b82f6;">BUSINESS_OBJECT</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CORRELATION</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">EVENT</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CUSTOM</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enabled -->
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||||
<div style="width:36px;height:20px;background:#3b82f6;border-radius:10px;position:relative;cursor:pointer;">
|
||||
<div style="width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
|
||||
</div>
|
||||
<span style="font-size:12px;color:#e0e0e0;">Enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,175 @@
|
||||
<h2>Add Tap — With Expression Testing</h2>
|
||||
<p class="subtitle">Collapsible test section at bottom of the tap modal</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Add Tap Modal — Test Expression (Recent Exchange)</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:560px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:15px;font-weight:600;">Add Tap</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:18px;">✕</span>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px;max-height:70vh;overflow-y:auto;">
|
||||
<!-- Form fields (collapsed for brevity) -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Name <span style="color:#f87171;">*</span></div>
|
||||
<input style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-size:12px;box-sizing:border-box;" value="orderId" />
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Processor <span style="color:#f87171;">*</span></div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:monospace;">unmarshal1</span>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:12px;margin-bottom:14px;">
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Language</div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>simple</span><span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target</div>
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span>OUTPUT</span><span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Expression <span style="color:#f87171;">*</span></div>
|
||||
<textarea style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical;min-height:40px;">${body.orderId}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Type</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div style="background:#1e3a5f;color:#7dd3fc;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #3b82f6;">BUSINESS_OBJECT</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CORRELATION</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">EVENT</div>
|
||||
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CUSTOM</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ TEST EXPRESSION SECTION ═══ -->
|
||||
<div style="border-top:1px solid #2d2d50;margin-top:8px;padding-top:14px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;cursor:pointer;">
|
||||
<span style="color:#60a5fa;font-size:10px;">▼</span>
|
||||
<span style="font-size:12px;font-weight:600;color:#60a5fa;">Test Expression</span>
|
||||
</div>
|
||||
|
||||
<!-- Data source tabs -->
|
||||
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
|
||||
<div style="padding:6px 14px;font-size:11px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Recent Exchange</div>
|
||||
<div style="padding:6px 14px;font-size:11px;color:#9ca3af;cursor:pointer;">Custom Payload</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent exchange picker -->
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;">
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:11px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="width:7px;height:7px;border-radius:50%;background:#4ade80;display:inline-block;"></span>
|
||||
<span style="font-family:monospace;color:#e0e0e0;">a1b2c3d4-e5f6-7890</span>
|
||||
<span style="color:#6b7280;">·</span>
|
||||
<span style="color:#6b7280;">245ms</span>
|
||||
<span style="color:#6b7280;">·</span>
|
||||
<span style="color:#6b7280;">2 min ago</span>
|
||||
</div>
|
||||
<span style="color:#9ca3af;font-size:10px;">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test button + result -->
|
||||
<div style="display:flex;gap:8px;align-items:flex-start;">
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;">▶ Test</button>
|
||||
<div style="flex:1;background:#0f2a1a;border:1px solid #166534;border-radius:6px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">Result</div>
|
||||
<div style="font-family:monospace;font-size:12px;color:#4ade80;">ORD-2024-78542</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;"></div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Test Expression — Custom Payload Mode</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:560px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
|
||||
<span style="font-size:15px;font-weight:600;">Add Tap</span>
|
||||
<span style="color:#9ca3af;cursor:pointer;font-size:18px;">✕</span>
|
||||
</div>
|
||||
|
||||
<div style="padding:20px;">
|
||||
<!-- Form fields abbreviated -->
|
||||
<div style="text-align:center;padding:8px;font-size:11px;color:#6b7280;border:1px dashed #2d2d50;border-radius:6px;margin-bottom:14px;">
|
||||
⬆ Form fields above (attribute name, processor, language, target, expression, type)
|
||||
</div>
|
||||
|
||||
<!-- ═══ TEST EXPRESSION SECTION ═══ -->
|
||||
<div style="border-top:1px solid #2d2d50;padding-top:14px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;cursor:pointer;">
|
||||
<span style="color:#60a5fa;font-size:10px;">▼</span>
|
||||
<span style="font-size:12px;font-weight:600;color:#60a5fa;">Test Expression</span>
|
||||
</div>
|
||||
|
||||
<!-- Data source tabs -->
|
||||
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
|
||||
<div style="padding:6px 14px;font-size:11px;color:#9ca3af;cursor:pointer;">Recent Exchange</div>
|
||||
<div style="padding:6px 14px;font-size:11px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Custom Payload</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom payload editor -->
|
||||
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;">
|
||||
<div style="margin-bottom:10px;">
|
||||
<textarea style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;resize:vertical;min-height:100px;line-height:1.5;">{
|
||||
"orderId": "ORD-2024-78542",
|
||||
"customer": {
|
||||
"id": "CUST-1234",
|
||||
"name": "Acme Corp"
|
||||
},
|
||||
"total": 149.90
|
||||
}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Test button + error result -->
|
||||
<div style="display:flex;gap:8px;align-items:flex-start;">
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;">▶ Test</button>
|
||||
<div style="flex:1;background:#2a0f0f;border:1px solid #991b1b;border-radius:6px;padding:8px 12px;">
|
||||
<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">Error</div>
|
||||
<div style="font-family:monospace;font-size:11px;color:#f87171;">Expression evaluation timed out (50ms limit)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#6b7280;margin-top:8px;">Evaluated by agent <span style="font-family:monospace;">order-svc-01</span> using Camel's <span style="font-family:monospace;">simple</span> language</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
|
||||
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
|
||||
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
<h2>Traces & Taps — With Route Column</h2>
|
||||
<p class="subtitle">Route column added to prevent ambiguity across routes</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Traces & Taps — Updated</div>
|
||||
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
|
||||
|
||||
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
|
||||
<span style="font-size:11px;color:#6b7280;">3 traced · 4 taps · manage taps on route pages</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid #2d2d50;">
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
|
||||
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;color:#60a5fa;font-size:11px;">processOrder</td>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;">unmarshal1</td>
|
||||
<td style="padding:8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
|
||||
<td style="padding:8px;">
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;color:#60a5fa;font-size:11px;">processOrder</td>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;">enrichPrice</td>
|
||||
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;">—</span></td>
|
||||
<td style="padding:8px;">
|
||||
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">✗</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #161630;">
|
||||
<td style="padding:8px;color:#60a5fa;font-size:11px;">processPayment</td>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;">toDatabase</td>
|
||||
<td style="padding:8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
|
||||
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;">—</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px;color:#60a5fa;font-size:11px;">processPayment</td>
|
||||
<td style="padding:8px;font-family:monospace;font-size:11px;">validate1</td>
|
||||
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;">—</span></td>
|
||||
<td style="padding:8px;">
|
||||
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">paymentRef <span style="color:#4ade80;margin-left:2px;">✓</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1774552065018}
|
||||
1
.superpowers/brainstorm/2048-1774541143/state/server.pid
Normal file
1
.superpowers/brainstorm/2048-1774541143/state/server.pid
Normal file
@@ -0,0 +1 @@
|
||||
2048
|
||||
@@ -36,9 +36,9 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
|
||||
- Spring Boot 3.4.3 parent POM
|
||||
- Depends on `com.cameleer3:cameleer3-common` from Gitea Maven registry
|
||||
- Jackson `JavaTimeModule` for `Instant` deserialization
|
||||
- Communication: receives HTTP POST data from agents, serves SSE event streams for config push/commands
|
||||
- Communication: receives HTTP POST data from agents (executions, diagrams, metrics, logs), serves SSE event streams for config push/commands (config-update, deep-trace, replay, route-control)
|
||||
- Maintains agent instance registry with states: LIVE → STALE → DEAD
|
||||
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search
|
||||
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search and application log storage
|
||||
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing, bootstrap token for registration
|
||||
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`server_config` table)
|
||||
- User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users`
|
||||
@@ -57,6 +57,10 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
|
||||
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready`, OpenSearch uses `/_cluster/health`
|
||||
- Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility
|
||||
|
||||
## UI Styling
|
||||
|
||||
- Always use `@cameleer/design-system` CSS variables for colors (`var(--amber)`, `var(--error)`, `var(--success)`, etc.) — never hardcode hex values. This applies to CSS modules, inline styles, and SVG `fill`/`stroke` attributes. SVG presentation attributes resolve `var()` correctly.
|
||||
|
||||
## Disabled Skills
|
||||
|
||||
- Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.
|
||||
|
||||
@@ -12,7 +12,7 @@ COPY cameleer3-server-app/pom.xml cameleer3-server-app/
|
||||
# Cache deps — only re-downloaded when POMs change
|
||||
RUN mvn dependency:go-offline -B || true
|
||||
COPY . .
|
||||
RUN mvn clean package -DskipTests -B
|
||||
RUN mvn clean package -DskipTests -U -B
|
||||
|
||||
FROM eclipse-temurin:17-jre
|
||||
WORKDIR /app
|
||||
|
||||
26
HOWTO.md
26
HOWTO.md
@@ -100,7 +100,7 @@ JWTs carry a `roles` claim. Endpoints are restricted by role:
|
||||
|
||||
| Role | Access |
|
||||
|------|--------|
|
||||
| `AGENT` | Data ingestion (`/data/**`), heartbeat, SSE events, command ack |
|
||||
| `AGENT` | Data ingestion (`/data/**` — executions, diagrams, metrics, logs), heartbeat, SSE events, command ack |
|
||||
| `VIEWER` | Search, execution detail, diagrams, agent list |
|
||||
| `OPERATOR` | VIEWER + send commands to agents |
|
||||
| `ADMIN` | OPERATOR + user management (`/admin/**`) |
|
||||
@@ -220,6 +220,20 @@ curl -s -X POST http://localhost:8081/api/v1/data/metrics \
|
||||
-H "X-Protocol-Version: 1" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '[{"agentId":"agent-1","metricName":"cpu","value":42.0,"timestamp":"2026-03-11T00:00:00Z","tags":{}}]'
|
||||
|
||||
# Post application log entries (batch)
|
||||
curl -s -X POST http://localhost:8081/api/v1/data/logs \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"entries": [{
|
||||
"timestamp": "2026-03-25T10:00:00Z",
|
||||
"level": "INFO",
|
||||
"loggerName": "com.acme.MyService",
|
||||
"message": "Processing order #12345",
|
||||
"threadName": "main"
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
**Note:** The `X-Protocol-Version: 1` header is required on all `/api/v1/data/**` endpoints. Missing or wrong version returns 400.
|
||||
@@ -311,6 +325,12 @@ curl -s -X POST http://localhost:8081/api/v1/agents/groups/order-service-prod/co
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"type":"deep-trace","payload":{"routeId":"route-1","durationSeconds":60}}'
|
||||
|
||||
# Send route control command to agent group (start/stop/suspend/resume)
|
||||
curl -s -X POST http://localhost:8081/api/v1/agents/groups/order-service-prod/commands \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"type":"route-control","payload":{"routeId":"route-1","action":"stop","nonce":"unique-uuid"}}'
|
||||
|
||||
# Broadcast command to all live agents
|
||||
curl -s -X POST http://localhost:8081/api/v1/agents/commands \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -324,7 +344,7 @@ curl -s -X POST http://localhost:8081/api/v1/agents/agent-1/commands/{commandId}
|
||||
|
||||
**Agent lifecycle:** LIVE (heartbeat within 90s) → STALE (missed 3 heartbeats) → DEAD (5min after STALE). DEAD agents kept indefinitely.
|
||||
|
||||
**SSE events:** `config-update`, `deep-trace`, `replay` commands pushed in real time. Server sends ping keepalive every 15s.
|
||||
**SSE events:** `config-update`, `deep-trace`, `replay`, `route-control` commands pushed in real time. Server sends ping keepalive every 15s.
|
||||
|
||||
**Command expiry:** Unacknowledged commands expire after 60 seconds.
|
||||
|
||||
@@ -361,6 +381,8 @@ Key settings in `cameleer3-server-app/src/main/resources/application.yml`:
|
||||
| `security.oidc.client-secret` | | OAuth2 client secret (`CAMELEER_OIDC_CLIENT_SECRET`) |
|
||||
| `security.oidc.roles-claim` | `realm_access.roles` | JSONPath to roles in OIDC id_token (`CAMELEER_OIDC_ROLES_CLAIM`) |
|
||||
| `security.oidc.default-roles` | `VIEWER` | Default roles for new OIDC users (`CAMELEER_OIDC_DEFAULT_ROLES`) |
|
||||
| `opensearch.log-index-prefix` | `logs-` | OpenSearch index prefix for application logs (`CAMELEER_LOG_INDEX_PREFIX`) |
|
||||
| `opensearch.log-retention-days` | `7` | Days before log indices are deleted (`CAMELEER_LOG_RETENTION_DAYS`) |
|
||||
|
||||
## Web UI Development
|
||||
|
||||
|
||||
294
UI_FINDINGS.md
Normal file
294
UI_FINDINGS.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# UI/UX Evaluation Report — Cameleer3 Server
|
||||
|
||||
**Date:** 2026-03-25
|
||||
**Evaluated URL:** http://192.168.50.86:30090/
|
||||
**Methodology:** Playwright-driven navigation of all major pages (14 screenshots), evaluated by 3 specialist agents: Visual Design, Information Architecture & Usability, Readability & Accessibility.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Cameleer3 dashboard has a **distinctive, well-crafted warm amber design language** that stands out in the observability space. The core monitoring pages (Dashboard, Exchange Detail, Routes, Agents) are polished and consistent. The design system provides a solid foundation.
|
||||
|
||||
**Key strengths:** KPI strip pattern, command palette (Ctrl+K), agent card grouping, dark mode token system, cohesive brand identity.
|
||||
|
||||
**Critical gaps to address:**
|
||||
1. **Font sizes too small** — pervasive 10-11px text for critical data impairs reading under stress
|
||||
2. **Color contrast failures** — `--text-muted` and `--text-faint` fail WCAG AA in both themes
|
||||
3. **Status indicators rely on color alone** — not accessible for color-blind users
|
||||
4. **Admin infrastructure pages lag in polish** — Database/OpenSearch use ad-hoc styling
|
||||
5. **Dashboard is a monitoring display, not yet an incident response tool** — missing error highlighting, per-route error breakdowns, actionable status pages
|
||||
|
||||
**Overall Score: 7/10** — Strong foundation, needs targeted fixes for production readiness under stress.
|
||||
|
||||
---
|
||||
|
||||
## Pages Evaluated
|
||||
|
||||
| # | Page | Screenshot |
|
||||
|---|------|-----------|
|
||||
| 1 | Login | `screenshots/14-login-page.png` |
|
||||
| 2 | Dashboard (light) | `screenshots/01-dashboard.png` |
|
||||
| 3 | Dashboard + Detail Panel | `screenshots/02-dashboard-detail-panel.png` |
|
||||
| 4 | Exchange Detail | `screenshots/03-exchange-detail.png` |
|
||||
| 5 | Routes Metrics | `screenshots/04-routes-metrics.png` |
|
||||
| 6 | Agent Health | `screenshots/05-agents.png` |
|
||||
| 7 | Agent Instance | `screenshots/06-agent-instance.png` |
|
||||
| 8 | Admin RBAC | `screenshots/07-admin-rbac.png` |
|
||||
| 9 | Admin Audit Log | `screenshots/08-admin-audit.png` |
|
||||
| 10 | Admin OIDC | `screenshots/09-admin-oidc.png` |
|
||||
| 11 | Admin Database | `screenshots/10-admin-database.png` |
|
||||
| 12 | Admin OpenSearch | `screenshots/11-admin-opensearch.png` |
|
||||
| 13 | Command Palette | `screenshots/12-command-palette.png` |
|
||||
| 14 | Dashboard (dark) | `screenshots/13-dashboard-dark-mode.png` |
|
||||
|
||||
---
|
||||
|
||||
## Page-by-Page Findings
|
||||
|
||||
### Login Page
|
||||
|
||||
- **[Important]** No brand identity — missing camel logo/icon from sidebar. First impression feels generic.
|
||||
- **[Important]** Sign-in button color mismatch — uses washed-out gold, not the saturated `--amber` (#C6820E) used throughout the app.
|
||||
- **[Important]** No SSO/OIDC button visible — system supports OIDC but login page only shows username/password.
|
||||
- **[Important]** Subtitle text `--text-muted` (#9C9184) on white fails WCAG AA (~2.8:1, needs 4.5:1).
|
||||
- **[Important]** White text on amber button fails WCAG AA for normal text (~3.2:1).
|
||||
- **[Nice-to-have]** Card has no shadow/border against the `--bg-body` cream background — minimal separation.
|
||||
|
||||
### Dashboard
|
||||
|
||||
- **[Important]** Errors KPI card uses red/orange accent border even when errors = 0. Zero-error state should feel reassuring (green/neutral), not alarming. Creates false alarm fatigue.
|
||||
- **[Important]** Table lacks visible sort indicators — no arrows showing current sort direction.
|
||||
- **[Important]** Duration column uses color alone (`.durFast` green, `.durSlow` amber, `.durBreach` red) — not color-blind safe.
|
||||
- **[Important]** Status dots are ~6px — too small to reliably identify, especially for color-blind users.
|
||||
- **[Critical]** Table meta text at 11px with `--text-muted` is borderline illegible for fatigued users.
|
||||
- **[Critical]** KPI stat labels at 10px with `--text-muted` — below recommended 12px minimum.
|
||||
- **[Nice-to-have]** Exchange ID column too wide — truncate to 8 chars with copy-on-click.
|
||||
|
||||
### Dashboard — Detail Panel
|
||||
|
||||
- **[Important]** Panel lacks clear visual separation from main table — needs left border accent or different background.
|
||||
- **[Important]** Processor timeline preview in panel is too small to read — adds visual noise without utility.
|
||||
- **[Critical]** Overview labels at 10px with `--text-muted` — nearly invisible.
|
||||
- **[Critical]** Panel section meta at 10px with `--text-faint` (#C4BAB0) on white — contrast ratio ~1.9:1, severely fails WCAG AA.
|
||||
- **[Nice-to-have]** No quick actions (copy exchange ID, view logs, view route diagram).
|
||||
|
||||
### Exchange Detail
|
||||
|
||||
- **[Critical]** Processor timeline label column too narrow — processor names are truncated/illegible. This is the page's primary visualization.
|
||||
- **[Critical]** No error highlighting in processor timeline — failed processors need red bars/icons. During incidents, engineers must instantly see WHICH processor failed.
|
||||
- **[Important]** No linkage to route diagram — "View in Route Diagram" would overlay execution on the visual route graph.
|
||||
- **[Important]** Long exchange ID in breadcrumb is visually heavy — truncate with copy button.
|
||||
- **[Important]** Header stat labels at 10px uppercase with `--text-muted` — same contrast issue.
|
||||
|
||||
### Routes Metrics
|
||||
|
||||
- **[Important]** KPI number formatting inconsistent — Dashboard shows "11.742 ms" (decimal + space), Routes shows "11742ms" (no decimal, no space).
|
||||
- **[Important]** No per-route error rate column — error rate in KPI strip but not broken down per route.
|
||||
- **[Important]** Charts disconnected from table — clicking a route should filter/highlight its chart data.
|
||||
- **[Nice-to-have]** No visual comparison between routes (bar chart or heatmap for quick identification of slowest).
|
||||
|
||||
### Agent Health
|
||||
|
||||
- **[Critical]** Stale/Dead agent visual distinction is too subtle — at 3am, the difference between LIVE and DEAD must scream. Dead agents should have prominent red background or strikethrough, not just `--text-muted`.
|
||||
- **[Critical]** Agent state dots (green live, amber stale, gray dead) use color alone — no shape variation for color-blind users.
|
||||
- **[Important]** "2/26" active routes KPI is ambiguous — unit and meaning need to be explicit.
|
||||
- **[Nice-to-have]** Timeline at bottom takes significant space — consider making it collapsible.
|
||||
|
||||
### Agent Instance Detail
|
||||
|
||||
- **[Important]** Charts lack threshold/alert lines — CPU at 2% is fine, but where is "concerning"? Configurable thresholds (CPU > 80%, Memory > 90%) would make charts actionable.
|
||||
- **[Important]** Chart axis labels appear too small.
|
||||
- **[Nice-to-have]** GC Pauses uses area fill while others use line charts — minor inconsistency.
|
||||
- **[Nice-to-have]** Six charts in 2x3 grid can create cognitive overload — consider collapsible groups.
|
||||
|
||||
### Admin — RBAC
|
||||
|
||||
- **[Important]** KPI strip for "Users: 1, Groups: 2, Roles: 4" has too much visual weight — these low-value numbers don't need full stat-card treatment.
|
||||
- **[Important]** "ADMIN" role badge vs "ADMINS" group badge look identical — different badge styles needed (outlined for groups, filled for roles).
|
||||
- **[Nice-to-have]** Empty detail panel ("Select a user to view details") needs icon/illustration.
|
||||
|
||||
### Admin — Audit Log
|
||||
|
||||
- **[Important]** "no data" empty state is uninformative — should explain "No audit events match your filters" with guidance.
|
||||
- **[Important]** No export functionality — audit logs need CSV/JSON export for compliance.
|
||||
- **[Important]** Date range filters use raw datetime inputs — inconsistent with dashboard's polished time range pills.
|
||||
|
||||
### Admin — OIDC Config
|
||||
|
||||
- **[Critical]** "Delete OIDC Configuration" is a destructive action without confirmation dialog — could lock out all SSO users.
|
||||
- **[Important]** No inline validation — Issuer URL should validate format on blur, required fields need indicators.
|
||||
- **[Nice-to-have]** No connection test result display area.
|
||||
|
||||
### Admin — Database
|
||||
|
||||
- **[Important]** Visual treatment inconsistent with rest of app — "Connected" status and pool stats use ad-hoc text, not design system components.
|
||||
- **[Important]** Page title "Database Administration" implies actions, but page is read-only — rename to "Database Status" or add operations.
|
||||
- **[Nice-to-have]** Table row counts should be right-aligned for numerical scanning.
|
||||
|
||||
### Admin — OpenSearch
|
||||
|
||||
- **[Critical]** "Disconnected" status displayed as plain text — needs error styling (red text, error badge, or status banner). Infrastructure disconnection is a critical state.
|
||||
- **[Important]** "Yellow" cluster health displayed as plain text with no visual hierarchy — same size/weight as version number and node count.
|
||||
- **[Important]** Indexing pipeline stats use ad-hoc inline format — should use consistent stat-card pattern.
|
||||
- **[Important]** "Disconnected" + "Yellow" health shown simultaneously is contradictory — if disconnected, clarify whether data is stale.
|
||||
|
||||
### Command Palette
|
||||
|
||||
- **[Nice-to-have]** No visible keyboard navigation hint for currently selected item.
|
||||
- **[Nice-to-have]** Empty palette should show recent/frequent items instead of requiring typing.
|
||||
- Overall well-executed — categories, counts, keyboard hints in footer.
|
||||
|
||||
### Dark Mode
|
||||
|
||||
- **[Critical]** `--text-muted` (#7A7068) on `--bg-surface` (#242019) is ~2.9:1 — fails WCAG AA. Affects ALL muted labels across every page.
|
||||
- **[Critical]** `--text-faint` (#4A4238) on `--bg-surface` (#242019) is ~1.4:1 — catastrophically fails WCAG AA. Essentially invisible.
|
||||
- **[Important]** `--amber` (#D4941E) on `--bg-surface` (#242019) is ~3.6:1 — amber links/active text fail AA.
|
||||
- **[Important]** KPI sparkline chart lines are harder to read — thin strokes need increased width or brightness.
|
||||
- **[Important]** Sidebar boundary contrast drops significantly (`--sidebar-bg` #141210 vs `--bg-body` #1A1714 is only ~6 units apart).
|
||||
- **[Important]** Table row alternation contrast near zero in dark mode.
|
||||
- **[Nice-to-have]** Amber accent color shift from #C6820E to #D4941E is well-handled.
|
||||
- **[Nice-to-have]** Semantic colors (success, error, warning) appropriately increase luminance.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Cutting Issues
|
||||
|
||||
### 1. Color Contrast (WCAG AA Failures)
|
||||
|
||||
**Light Mode:**
|
||||
|
||||
| Element | Foreground | Background | Ratio | Required | Verdict |
|
||||
|---------|-----------|------------|-------|----------|---------|
|
||||
| StatCard labels, table meta, section headers | `--text-muted` #9C9184 | #FFFFFF | ~3.0:1 | 4.5:1 | **FAIL** |
|
||||
| Panel meta, overview hints | `--text-faint` #C4BAB0 | #FFFFFF | ~1.9:1 | 4.5:1 | **FAIL** |
|
||||
| Sign-in button text | #FFFFFF | `--amber` #C6820E | ~3.2:1 | 4.5:1 | **FAIL** |
|
||||
| Sidebar muted text | #9C9184 | `--sidebar-bg` #2C2520 | ~3.1:1 | 4.5:1 | **FAIL** |
|
||||
|
||||
**Dark Mode:**
|
||||
|
||||
| Element | Foreground | Background | Ratio | Required | Verdict |
|
||||
|---------|-----------|------------|-------|----------|---------|
|
||||
| All muted labels | #7A7068 | #242019 | ~2.9:1 | 4.5:1 | **FAIL** |
|
||||
| All faint hints | #4A4238 | #242019 | ~1.4:1 | 4.5:1 | **FAIL** |
|
||||
| Amber links/active text | #D4941E | #242019 | ~3.6:1 | 4.5:1 | **FAIL** |
|
||||
|
||||
**Fix:** Change `--text-muted` to **#766A5E** (light) / **#9A9088** (dark). Restrict `--text-faint` to decorative use only or lighten dark variant to #6A6058.
|
||||
|
||||
### 2. Font Size Floor
|
||||
|
||||
10px text is used for: StatCard labels, overview labels, chain labels, section meta, error class names, detail labels, sidebar tree labels. 11px is used for: table meta, error messages, pagination, toggle buttons, chart titles.
|
||||
|
||||
**Fix:** Establish `--font-size-min: 12px` as a design system floor. Update all 10px instances to 12px, all 11px instances to 12px.
|
||||
|
||||
### 3. Number/Unit Formatting
|
||||
|
||||
Inconsistent across pages:
|
||||
- Dashboard: "11.742 ms" (decimal + space)
|
||||
- Routes: "11742ms" (no decimal, no space)
|
||||
- Dashboard: "1.1 msg/s" vs Agent Instance: "0.1/s"
|
||||
|
||||
**Fix:** Create a shared formatting utility enforcing: consistent decimal precision, space before unit, consistent abbreviations.
|
||||
|
||||
### 4. KPI Strip Inconsistency
|
||||
|
||||
Used on Dashboard, Routes, Agents, Agent Instance (consistent). But RBAC uses oversized cards for trivial counts, and Database/OpenSearch use ad-hoc text rendering.
|
||||
|
||||
**Fix:** Admin infra pages should adopt KPI stat strip or a compact-stat component.
|
||||
|
||||
### 5. Empty States
|
||||
|
||||
Inconsistent handling:
|
||||
- Audit Log: "no data" in plain gray
|
||||
- RBAC detail: "Select a user to view details" in gray
|
||||
- No consistent empty state component with icon + message + CTA
|
||||
|
||||
**Fix:** Design system EmptyState component with icon, message, and optional action.
|
||||
|
||||
### 6. Status Indicator Accessibility
|
||||
|
||||
Color-only status encoding throughout:
|
||||
- Duration: green (fast), amber (slow), red (breach) — no icons
|
||||
- Status dots: green (live), amber (stale), gray (dead) — no shapes
|
||||
- Agent dead state uses `--text-muted` instead of `--error`
|
||||
|
||||
**Fix:** Add shape variation (checkmark/triangle/X), increase dot size to 10px minimum, always render text label alongside.
|
||||
|
||||
### 7. Sidebar Structure
|
||||
|
||||
Same apps listed 3x (under Applications, Agents, Routes) — triples sidebar length and scales poorly.
|
||||
|
||||
**Fix:** Unified application-centric tree where expanding an app shows its agents and routes as children.
|
||||
|
||||
---
|
||||
|
||||
## Prioritized Recommendations
|
||||
|
||||
### Critical (fix now)
|
||||
|
||||
| # | Recommendation | Impact |
|
||||
|---|---------------|--------|
|
||||
| 1 | **Bump `--text-muted` to WCAG AA compliance** — #766A5E (light) / #9A9088 (dark). Single highest-impact fix across all pages. | Fixes majority of contrast failures |
|
||||
| 2 | **Establish 12px minimum font size** — update all 10px and 11px instances. Especially StatCard labels, overview labels, table meta. | Readable under stress |
|
||||
| 3 | **Add error highlighting to processor timeline** — red bars, error icons for failed processors. Core debugging view. | Incident response speed |
|
||||
| 4 | **Make Stale/Dead agent states unmistakable** — full card background color (yellow stale, red dead), prominent badge. Change dead from `--text-muted` to `--error`. | Prevents missed outages |
|
||||
| 5 | **Fix OpenSearch "Disconnected" status** — use error badge/banner, add "Reconnect" action, clarify stale data. | Actionable admin page |
|
||||
| 6 | **Add confirmation dialog for OIDC deletion** — type-to-confirm to prevent locking out SSO users. | Prevents lockout |
|
||||
| 7 | **Color Errors KPI card conditionally** — green/neutral at 0, red only when > 0. Prevents false alarm fatigue. | Reduces noise |
|
||||
|
||||
### Important (next sprint)
|
||||
|
||||
| # | Recommendation | Impact |
|
||||
|---|---------------|--------|
|
||||
| 8 | **Add secondary encoding to status indicators** — shapes (checkmark/triangle/X) alongside color dots. Increase dot size to 10px+. | Accessibility compliance |
|
||||
| 9 | **Standardize number/unit formatting** — shared utility for decimals, spacing, unit abbreviations. | Visual consistency |
|
||||
| 10 | **Add per-route error rate to Routes table** — essential for isolating failing routes. | Incident triage |
|
||||
| 11 | **Add visible sort indicators to data tables** — arrows on column headers. | Data exploration |
|
||||
| 12 | **Bring admin infra pages to design system quality** — replace ad-hoc text with KPI strips/stat cards. | Professional polish |
|
||||
| 13 | **Fix login page brand identity** — add camel logo, use correct `--amber` for button, add SSO button when OIDC configured. | First impression |
|
||||
| 14 | **Fix dark mode specifics** — increase sidebar boundary contrast (add 1px border), boost chart stroke width, fix amber link contrast. | Dark mode usability |
|
||||
| 15 | **Widen processor timeline label column** — prevent name truncation, add tooltips for long names. | Core visualization |
|
||||
| 16 | **Add detail panel visual separation** — 2px left border accent. | Layout clarity |
|
||||
| 17 | **Pin Admin/API Docs to sidebar footer** — accessible without scrolling. | Navigation |
|
||||
| 18 | **Audit log improvements** — informative empty state, CSV/JSON export, date picker consistent with dashboard. | Admin usability |
|
||||
| 19 | **OIDC form validation** — inline URL validation, required field indicators, test result display. | Configuration safety |
|
||||
| 20 | **Fix amber button text contrast** — darken button to #8B5A06 or use dark text on amber. | Accessibility |
|
||||
|
||||
### Nice-to-have (backlog)
|
||||
|
||||
| # | Recommendation | Impact |
|
||||
|---|---------------|--------|
|
||||
| 21 | Unify sidebar into single application-centric tree (Applications > agents + routes) | Scalability |
|
||||
| 22 | Truncate Exchange IDs to 8 chars with copy-on-click | Table space |
|
||||
| 23 | Add threshold/alert lines to agent metric charts | Actionable monitoring |
|
||||
| 24 | Link charts to table selection on Routes Metrics | Data exploration |
|
||||
| 25 | Add clickable KPI cards navigating to filtered views | Navigation shortcuts |
|
||||
| 26 | Add `prefers-reduced-motion` support for StatusDot pulse animation | Accessibility |
|
||||
| 27 | Add tooltips to sparkline charts showing value on hover | Data context |
|
||||
| 28 | Replace hardcoded `#5db866` in Dashboard.module.css with `var(--success)` | Token compliance |
|
||||
| 29 | Add keyboard navigation indicators to command palette (selected item highlight) | Power user UX |
|
||||
| 30 | Show recent/frequent items in empty command palette | Discoverability |
|
||||
| 31 | Consolidate duplicated table-header CSS into design system component | Maintainability |
|
||||
| 32 | Login page card shadow for visual lift | Polish |
|
||||
| 33 | Collapsible agent event timeline | Space efficiency |
|
||||
| 34 | Dark mode `--text-faint` increase to #6A6058 for 3:1 minimum | Accessibility |
|
||||
| 35 | Increase DataTable row height to 44px (touch target minimum) | Accessibility |
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode Assessment
|
||||
|
||||
**Grade: Good foundation, specific contrast concerns.**
|
||||
|
||||
**What works well:**
|
||||
- Token system remaps all semantic colors without introducing cold blue-grays — warm brand preserved
|
||||
- Amber accent brightens appropriately (#C6820E → #D4941E)
|
||||
- Error/warning/success colors increase luminance correctly
|
||||
- Shadows shift from warm semi-transparent to opaque — correct for dark backgrounds
|
||||
|
||||
**What needs fixing:**
|
||||
- Sidebar contrast: `--sidebar-bg` #141210 vs `--bg-body` #1A1714 only ~6 units apart (was ~50 in light mode)
|
||||
- Chart line visibility: thin 1-2px strokes need increased width
|
||||
- Table row alternation: near-zero contrast between `--bg-surface` and `--bg-raised`
|
||||
- `--text-faint`: essentially invisible at 1.4:1 contrast
|
||||
- `--text-muted`: 2.9:1 — below AA minimum
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cameleer3.server.app.config;
|
||||
|
||||
import com.cameleer3.server.app.interceptor.AuditInterceptor;
|
||||
import com.cameleer3.server.app.interceptor.ProtocolVersionInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
@@ -7,17 +8,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC configuration.
|
||||
* <p>
|
||||
* Registers the {@link ProtocolVersionInterceptor} on data and agent endpoint paths,
|
||||
* excluding health, API docs, and Swagger UI paths that do not require protocol versioning.
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final ProtocolVersionInterceptor protocolVersionInterceptor;
|
||||
private final AuditInterceptor auditInterceptor;
|
||||
|
||||
public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor) {
|
||||
public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor,
|
||||
AuditInterceptor auditInterceptor) {
|
||||
this.protocolVersionInterceptor = protocolVersionInterceptor;
|
||||
this.auditInterceptor = auditInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,5 +34,14 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
"/api/v1/agents/register",
|
||||
"/api/v1/agents/*/refresh"
|
||||
);
|
||||
|
||||
// Safety-net audit: catches any unaudited POST/PUT/DELETE
|
||||
registry.addInterceptor(auditInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
.excludePathPatterns(
|
||||
"/api/v1/data/**",
|
||||
"/api/v1/agents/*/heartbeat",
|
||||
"/api/v1/health"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.agent.SseConnectionManager;
|
||||
import com.cameleer3.server.app.dto.CommandAckRequest;
|
||||
import com.cameleer3.server.app.dto.CommandBroadcastResponse;
|
||||
import com.cameleer3.server.app.dto.CommandRequest;
|
||||
import com.cameleer3.server.app.dto.CommandSingleResponse;
|
||||
import com.cameleer3.server.app.dto.ReplayRequest;
|
||||
import com.cameleer3.server.app.dto.ReplayResponse;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.agent.AgentCommand;
|
||||
import com.cameleer3.server.core.agent.AgentEventService;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.agent.AgentState;
|
||||
import com.cameleer3.server.core.agent.CommandReply;
|
||||
import com.cameleer3.server.core.agent.CommandType;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -26,7 +35,14 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Command push endpoints for sending commands to agents via SSE.
|
||||
@@ -48,23 +64,30 @@ public class AgentCommandController {
|
||||
private final AgentRegistryService registryService;
|
||||
private final SseConnectionManager connectionManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AgentEventService agentEventService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public AgentCommandController(AgentRegistryService registryService,
|
||||
SseConnectionManager connectionManager,
|
||||
ObjectMapper objectMapper) {
|
||||
ObjectMapper objectMapper,
|
||||
AgentEventService agentEventService,
|
||||
AuditService auditService) {
|
||||
this.registryService = registryService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.objectMapper = objectMapper;
|
||||
this.agentEventService = agentEventService;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/commands")
|
||||
@Operation(summary = "Send command to a specific agent",
|
||||
description = "Sends a config-update, deep-trace, or replay command to the specified agent")
|
||||
description = "Sends a command to the specified agent via SSE")
|
||||
@ApiResponse(responseCode = "202", description = "Command accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
@ApiResponse(responseCode = "404", description = "Agent not registered")
|
||||
public ResponseEntity<CommandSingleResponse> sendCommand(@PathVariable String id,
|
||||
@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
@RequestBody CommandRequest request,
|
||||
HttpServletRequest httpRequest) throws JsonProcessingException {
|
||||
AgentInfo agent = registryService.findById(id);
|
||||
if (agent == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
|
||||
@@ -76,6 +99,10 @@ public class AgentCommandController {
|
||||
|
||||
String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING";
|
||||
|
||||
auditService.log("send_agent_command", AuditCategory.AGENT, id,
|
||||
java.util.Map.of("type", request.type(), "status", status),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(new CommandSingleResponse(command.id(), status));
|
||||
}
|
||||
@@ -86,7 +113,8 @@ public class AgentCommandController {
|
||||
@ApiResponse(responseCode = "202", description = "Commands accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
public ResponseEntity<CommandBroadcastResponse> sendGroupCommand(@PathVariable String group,
|
||||
@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
@RequestBody CommandRequest request,
|
||||
HttpServletRequest httpRequest) throws JsonProcessingException {
|
||||
CommandType type = mapCommandType(request.type());
|
||||
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
|
||||
|
||||
@@ -101,6 +129,10 @@ public class AgentCommandController {
|
||||
commandIds.add(command.id());
|
||||
}
|
||||
|
||||
auditService.log("broadcast_group_command", AuditCategory.AGENT, group,
|
||||
java.util.Map.of("type", request.type(), "agentCount", agents.size()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(new CommandBroadcastResponse(commandIds, agents.size()));
|
||||
}
|
||||
@@ -110,7 +142,8 @@ public class AgentCommandController {
|
||||
description = "Sends a command to all agents currently in LIVE state")
|
||||
@ApiResponse(responseCode = "202", description = "Commands accepted")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid command payload")
|
||||
public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestBody CommandRequest request) throws JsonProcessingException {
|
||||
public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestBody CommandRequest request,
|
||||
HttpServletRequest httpRequest) throws JsonProcessingException {
|
||||
CommandType type = mapCommandType(request.type());
|
||||
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
|
||||
|
||||
@@ -122,31 +155,124 @@ public class AgentCommandController {
|
||||
commandIds.add(command.id());
|
||||
}
|
||||
|
||||
auditService.log("broadcast_all_command", AuditCategory.AGENT, null,
|
||||
java.util.Map.of("type", request.type(), "agentCount", liveAgents.size()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED)
|
||||
.body(new CommandBroadcastResponse(commandIds, liveAgents.size()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/commands/{commandId}/ack")
|
||||
@Operation(summary = "Acknowledge command receipt",
|
||||
description = "Agent acknowledges that it has received and processed a command")
|
||||
description = "Agent acknowledges that it has received and processed a command, with result status and message")
|
||||
@ApiResponse(responseCode = "200", description = "Command acknowledged")
|
||||
@ApiResponse(responseCode = "404", description = "Command not found")
|
||||
public ResponseEntity<Void> acknowledgeCommand(@PathVariable String id,
|
||||
@PathVariable String commandId) {
|
||||
@PathVariable String commandId,
|
||||
@RequestBody(required = false) CommandAckRequest body) {
|
||||
boolean acknowledged = registryService.acknowledgeCommand(id, commandId);
|
||||
if (!acknowledged) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Command not found: " + commandId);
|
||||
}
|
||||
|
||||
// Complete any pending reply future (for synchronous request-reply commands like TEST_EXPRESSION)
|
||||
registryService.completeReply(commandId,
|
||||
body != null ? body.status() : "SUCCESS",
|
||||
body != null ? body.message() : null,
|
||||
body != null ? body.data() : null);
|
||||
|
||||
// Record command result in agent event log
|
||||
if (body != null && body.status() != null) {
|
||||
AgentInfo agent = registryService.findById(id);
|
||||
String application = agent != null ? agent.application() : "unknown";
|
||||
agentEventService.recordEvent(id, application, "COMMAND_" + body.status(),
|
||||
"Command " + commandId + ": " + body.message());
|
||||
log.debug("Command {} ack from agent {}: {} - {}", commandId, id, body.status(), body.message());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/replay")
|
||||
@Operation(summary = "Replay an exchange on a specific agent (synchronous)",
|
||||
description = "Sends a replay command and waits for the agent to complete the replay. "
|
||||
+ "Returns the replay result including status, replayExchangeId, and duration.")
|
||||
@ApiResponse(responseCode = "200", description = "Replay completed (check status for success/failure)")
|
||||
@ApiResponse(responseCode = "404", description = "Agent not found or not connected")
|
||||
@ApiResponse(responseCode = "504", description = "Agent did not respond in time")
|
||||
public ResponseEntity<ReplayResponse> replayExchange(@PathVariable String id,
|
||||
@RequestBody ReplayRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
AgentInfo agent = registryService.findById(id);
|
||||
if (agent == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
|
||||
}
|
||||
|
||||
// Build protocol-compliant replay payload
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("routeId", request.routeId());
|
||||
Map<String, Object> exchange = new LinkedHashMap<>();
|
||||
exchange.put("body", request.body() != null ? request.body() : "");
|
||||
exchange.put("headers", request.headers() != null ? request.headers() : Map.of());
|
||||
payload.put("exchange", exchange);
|
||||
if (request.originalExchangeId() != null) {
|
||||
payload.put("originalExchangeId", request.originalExchangeId());
|
||||
}
|
||||
payload.put("nonce", UUID.randomUUID().toString());
|
||||
|
||||
String payloadJson;
|
||||
try {
|
||||
payloadJson = objectMapper.writeValueAsString(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize replay payload", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ReplayResponse("FAILURE", "Failed to serialize request", null));
|
||||
}
|
||||
|
||||
CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
|
||||
id, CommandType.REPLAY, payloadJson);
|
||||
|
||||
Map<String, Object> auditDetails = new LinkedHashMap<>();
|
||||
auditDetails.put("routeId", request.routeId());
|
||||
if (request.originalExchangeId() != null) {
|
||||
auditDetails.put("originalExchangeId", request.originalExchangeId());
|
||||
}
|
||||
|
||||
try {
|
||||
CommandReply reply = future.orTimeout(30, TimeUnit.SECONDS).join();
|
||||
auditDetails.put("replyStatus", reply.status());
|
||||
auditDetails.put("replyMessage", reply.message() != null ? reply.message() : "");
|
||||
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
|
||||
"SUCCESS".equals(reply.status()) ? AuditResult.SUCCESS : AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.ok(new ReplayResponse(reply.status(), reply.message(), reply.data()));
|
||||
} catch (CompletionException e) {
|
||||
if (e.getCause() instanceof TimeoutException) {
|
||||
auditDetails.put("error", "timeout");
|
||||
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
|
||||
.body(new ReplayResponse("FAILURE", "Agent did not respond within 30 seconds", null));
|
||||
}
|
||||
auditDetails.put("error", e.getCause().getMessage());
|
||||
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
log.error("Error awaiting replay reply from agent {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ReplayResponse("FAILURE", "Internal error: " + e.getCause().getMessage(), null));
|
||||
}
|
||||
}
|
||||
|
||||
private CommandType mapCommandType(String typeStr) {
|
||||
return switch (typeStr) {
|
||||
case "config-update" -> CommandType.CONFIG_UPDATE;
|
||||
case "deep-trace" -> CommandType.DEEP_TRACE;
|
||||
case "replay" -> CommandType.REPLAY;
|
||||
case "set-traced-processors" -> CommandType.SET_TRACED_PROCESSORS;
|
||||
case "test-expression" -> CommandType.TEST_EXPRESSION;
|
||||
case "route-control" -> CommandType.ROUTE_CONTROL;
|
||||
default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay");
|
||||
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay, set-traced-processors, test-expression, route-control");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import com.cameleer3.server.app.dto.AgentRegistrationRequest;
|
||||
import com.cameleer3.server.app.dto.AgentRegistrationResponse;
|
||||
import com.cameleer3.server.app.dto.ErrorResponse;
|
||||
import com.cameleer3.server.app.security.BootstrapTokenValidator;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.agent.AgentEventService;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
@@ -58,6 +61,7 @@ public class AgentRegistrationController {
|
||||
private final JwtService jwtService;
|
||||
private final Ed25519SigningService ed25519SigningService;
|
||||
private final AgentEventService agentEventService;
|
||||
private final AuditService auditService;
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
public AgentRegistrationController(AgentRegistryService registryService,
|
||||
@@ -66,6 +70,7 @@ public class AgentRegistrationController {
|
||||
JwtService jwtService,
|
||||
Ed25519SigningService ed25519SigningService,
|
||||
AgentEventService agentEventService,
|
||||
AuditService auditService,
|
||||
JdbcTemplate jdbc) {
|
||||
this.registryService = registryService;
|
||||
this.config = config;
|
||||
@@ -73,6 +78,7 @@ public class AgentRegistrationController {
|
||||
this.jwtService = jwtService;
|
||||
this.ed25519SigningService = ed25519SigningService;
|
||||
this.agentEventService = agentEventService;
|
||||
this.auditService = auditService;
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
@@ -113,6 +119,10 @@ public class AgentRegistrationController {
|
||||
agentEventService.recordEvent(request.agentId(), application, "REGISTERED",
|
||||
"Agent registered: " + request.name());
|
||||
|
||||
auditService.log(request.agentId(), "agent_register", AuditCategory.AGENT, request.agentId(),
|
||||
Map.of("application", application, "name", request.name()),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
// Issue JWT tokens with AGENT role
|
||||
List<String> roles = List.of("AGENT");
|
||||
String accessToken = jwtService.createAccessToken(request.agentId(), application, roles);
|
||||
@@ -135,7 +145,8 @@ public class AgentRegistrationController {
|
||||
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
|
||||
@ApiResponse(responseCode = "404", description = "Agent not found")
|
||||
public ResponseEntity<AgentRefreshResponse> refresh(@PathVariable String id,
|
||||
@RequestBody AgentRefreshRequest request) {
|
||||
@RequestBody AgentRefreshRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
if (request.refreshToken() == null || request.refreshToken().isBlank()) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
@@ -169,6 +180,9 @@ public class AgentRegistrationController {
|
||||
String newAccessToken = jwtService.createAccessToken(agentId, agent.application(), roles);
|
||||
String newRefreshToken = jwtService.createRefreshToken(agentId, agent.application(), roles);
|
||||
|
||||
auditService.log(agentId, "agent_token_refresh", AuditCategory.AUTH, agentId,
|
||||
null, AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(new AgentRefreshResponse(newAccessToken, newRefreshToken));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ public class ApiExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
|
||||
String reason = ex.getReason();
|
||||
return ResponseEntity.status(ex.getStatusCode())
|
||||
.body(new ErrorResponse(ex.getReason() != null ? ex.getReason() : "Unknown error"));
|
||||
.body(new ErrorResponse(reason != null ? reason : "Unknown error"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.dto.AppSettingsRequest;
|
||||
import com.cameleer3.server.core.admin.AppSettings;
|
||||
import com.cameleer3.server.core.admin.AppSettingsRepository;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/app-settings")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
|
||||
@Tag(name = "App Settings", description = "Per-application dashboard settings (ADMIN/OPERATOR)")
|
||||
public class AppSettingsController {
|
||||
|
||||
private final AppSettingsRepository repository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public AppSettingsController(AppSettingsRepository repository, AuditService auditService) {
|
||||
this.repository = repository;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List all application settings")
|
||||
public ResponseEntity<List<AppSettings>> getAll() {
|
||||
return ResponseEntity.ok(repository.findAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{appId}")
|
||||
@Operation(summary = "Get settings for a specific application (returns defaults if not configured)")
|
||||
public ResponseEntity<AppSettings> getByAppId(@PathVariable String appId) {
|
||||
AppSettings settings = repository.findByAppId(appId).orElse(AppSettings.defaults(appId));
|
||||
return ResponseEntity.ok(settings);
|
||||
}
|
||||
|
||||
@PutMapping("/{appId}")
|
||||
@Operation(summary = "Create or update settings for an application")
|
||||
public ResponseEntity<AppSettings> update(@PathVariable String appId,
|
||||
@Valid @RequestBody AppSettingsRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
List<String> errors = request.validate();
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.join("; ", errors));
|
||||
}
|
||||
|
||||
AppSettings saved = repository.save(request.toSettings(appId));
|
||||
auditService.log("update_app_settings", AuditCategory.CONFIG, appId,
|
||||
Map.of("settings", saved), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(saved);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{appId}")
|
||||
@Operation(summary = "Delete application settings (reverts to defaults)")
|
||||
public ResponseEntity<Void> delete(@PathVariable String appId, HttpServletRequest httpRequest) {
|
||||
repository.delete(appId);
|
||||
auditService.log("delete_app_settings", AuditCategory.CONFIG, appId,
|
||||
Map.of(), AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.common.model.ApplicationConfig;
|
||||
import com.cameleer3.server.app.dto.TestExpressionRequest;
|
||||
import com.cameleer3.server.app.dto.TestExpressionResponse;
|
||||
import com.cameleer3.server.app.storage.PostgresApplicationConfigRepository;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.agent.AgentCommand;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.agent.AgentState;
|
||||
import com.cameleer3.server.core.agent.CommandReply;
|
||||
import com.cameleer3.server.core.agent.CommandType;
|
||||
import com.cameleer3.server.core.storage.DiagramStore;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Per-application configuration management.
|
||||
* Agents fetch config at startup; the UI modifies config which is persisted and pushed to agents via SSE.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/config")
|
||||
@Tag(name = "Application Config", description = "Per-application observability configuration")
|
||||
public class ApplicationConfigController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApplicationConfigController.class);
|
||||
|
||||
private final PostgresApplicationConfigRepository configRepository;
|
||||
private final AgentRegistryService registryService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AuditService auditService;
|
||||
private final DiagramStore diagramStore;
|
||||
|
||||
public ApplicationConfigController(PostgresApplicationConfigRepository configRepository,
|
||||
AgentRegistryService registryService,
|
||||
ObjectMapper objectMapper,
|
||||
AuditService auditService,
|
||||
DiagramStore diagramStore) {
|
||||
this.configRepository = configRepository;
|
||||
this.registryService = registryService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.auditService = auditService;
|
||||
this.diagramStore = diagramStore;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "List all application configs",
|
||||
description = "Returns stored configurations for all applications")
|
||||
@ApiResponse(responseCode = "200", description = "Configs returned")
|
||||
public ResponseEntity<List<ApplicationConfig>> listConfigs(HttpServletRequest httpRequest) {
|
||||
auditService.log("view_app_configs", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(configRepository.findAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{application}")
|
||||
@Operation(summary = "Get application config",
|
||||
description = "Returns the current configuration for an application. Returns defaults if none stored.")
|
||||
@ApiResponse(responseCode = "200", description = "Config returned")
|
||||
public ResponseEntity<ApplicationConfig> getConfig(@PathVariable String application,
|
||||
HttpServletRequest httpRequest) {
|
||||
auditService.log("view_app_config", AuditCategory.CONFIG, application, null, AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(
|
||||
configRepository.findByApplication(application)
|
||||
.orElse(defaultConfig(application)));
|
||||
}
|
||||
|
||||
@PutMapping("/{application}")
|
||||
@Operation(summary = "Update application config",
|
||||
description = "Saves config and pushes CONFIG_UPDATE to all LIVE agents of this application")
|
||||
@ApiResponse(responseCode = "200", description = "Config saved and pushed")
|
||||
public ResponseEntity<ApplicationConfig> updateConfig(@PathVariable String application,
|
||||
@RequestBody ApplicationConfig config,
|
||||
Authentication auth,
|
||||
HttpServletRequest httpRequest) {
|
||||
String updatedBy = auth != null ? auth.getName() : "system";
|
||||
|
||||
config.setApplication(application);
|
||||
ApplicationConfig saved = configRepository.save(application, config, updatedBy);
|
||||
|
||||
int pushed = pushConfigToAgents(application, saved);
|
||||
log.info("Config v{} saved for '{}', pushed to {} agent(s)", saved.getVersion(), application, pushed);
|
||||
|
||||
auditService.log("update_app_config", AuditCategory.CONFIG, application,
|
||||
Map.of("version", saved.getVersion(), "agentsPushed", pushed),
|
||||
AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
return ResponseEntity.ok(saved);
|
||||
}
|
||||
|
||||
@GetMapping("/{application}/processor-routes")
|
||||
@Operation(summary = "Get processor to route mapping",
|
||||
description = "Returns a map of processorId → routeId for all processors seen in this application")
|
||||
@ApiResponse(responseCode = "200", description = "Mapping returned")
|
||||
public ResponseEntity<Map<String, String>> getProcessorRouteMapping(@PathVariable String application) {
|
||||
return ResponseEntity.ok(diagramStore.findProcessorRouteMapping(application));
|
||||
}
|
||||
|
||||
@PostMapping("/{application}/test-expression")
|
||||
@Operation(summary = "Test a tap expression against sample data via a live agent")
|
||||
@ApiResponse(responseCode = "200", description = "Expression evaluated successfully")
|
||||
@ApiResponse(responseCode = "404", description = "No live agent available for this application")
|
||||
@ApiResponse(responseCode = "504", description = "Agent did not respond in time")
|
||||
public ResponseEntity<TestExpressionResponse> testExpression(
|
||||
@PathVariable String application,
|
||||
@RequestBody TestExpressionRequest request) {
|
||||
// Find a LIVE agent for this application
|
||||
AgentInfo agent = registryService.findAll().stream()
|
||||
.filter(a -> application.equals(a.application()))
|
||||
.filter(a -> a.state() == AgentState.LIVE)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (agent == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new TestExpressionResponse(null, "No live agent available for application: " + application));
|
||||
}
|
||||
|
||||
// Build payload JSON
|
||||
String payloadJson;
|
||||
try {
|
||||
payloadJson = objectMapper.writeValueAsString(Map.of(
|
||||
"expression", request.expression() != null ? request.expression() : "",
|
||||
"language", request.language() != null ? request.language() : "",
|
||||
"body", request.body() != null ? request.body() : "",
|
||||
"target", request.target() != null ? request.target() : ""
|
||||
));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize test-expression payload", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new TestExpressionResponse(null, "Failed to serialize request"));
|
||||
}
|
||||
|
||||
// Send command and await reply
|
||||
CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
|
||||
agent.id(), CommandType.TEST_EXPRESSION, payloadJson);
|
||||
|
||||
try {
|
||||
CommandReply reply = future.orTimeout(5, TimeUnit.SECONDS).join();
|
||||
if ("SUCCESS".equals(reply.status())) {
|
||||
return ResponseEntity.ok(new TestExpressionResponse(reply.data(), null));
|
||||
} else {
|
||||
return ResponseEntity.ok(new TestExpressionResponse(null, reply.message()));
|
||||
}
|
||||
} catch (CompletionException e) {
|
||||
if (e.getCause() instanceof TimeoutException) {
|
||||
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
|
||||
.body(new TestExpressionResponse(null, "Agent did not respond within 5 seconds"));
|
||||
}
|
||||
log.error("Error awaiting test-expression reply from agent {}", agent.id(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new TestExpressionResponse(null, "Internal error: " + e.getCause().getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private int pushConfigToAgents(String application, ApplicationConfig config) {
|
||||
String payloadJson;
|
||||
try {
|
||||
payloadJson = objectMapper.writeValueAsString(config);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize config for push", e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<AgentInfo> agents = registryService.findAll().stream()
|
||||
.filter(a -> a.state() == AgentState.LIVE)
|
||||
.filter(a -> application.equals(a.application()))
|
||||
.toList();
|
||||
|
||||
for (AgentInfo agent : agents) {
|
||||
registryService.addCommand(agent.id(), CommandType.CONFIG_UPDATE, payloadJson);
|
||||
}
|
||||
return agents.size();
|
||||
}
|
||||
|
||||
private static ApplicationConfig defaultConfig(String application) {
|
||||
ApplicationConfig config = new ApplicationConfig();
|
||||
config.setApplication(application);
|
||||
config.setVersion(0);
|
||||
config.setMetricsEnabled(true);
|
||||
config.setSamplingRate(1.0);
|
||||
config.setTracedProcessors(Map.of());
|
||||
config.setApplicationLogLevel("INFO");
|
||||
config.setAgentLogLevel("INFO");
|
||||
config.setEngineLevel("REGULAR");
|
||||
config.setPayloadCaptureMode("NONE");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,11 @@ import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditRepository;
|
||||
import com.cameleer3.server.core.admin.AuditRepository.AuditPage;
|
||||
import com.cameleer3.server.core.admin.AuditRepository.AuditQuery;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -16,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/audit")
|
||||
@@ -26,19 +27,22 @@ import java.time.ZoneOffset;
|
||||
public class AuditLogController {
|
||||
|
||||
private final AuditRepository auditRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public AuditLogController(AuditRepository auditRepository) {
|
||||
public AuditLogController(AuditRepository auditRepository, AuditService auditService) {
|
||||
this.auditRepository = auditRepository;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Search audit log entries with pagination")
|
||||
public ResponseEntity<AuditLogPageResponse> getAuditLog(
|
||||
HttpServletRequest httpRequest,
|
||||
@RequestParam(required = false) String username,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to,
|
||||
@RequestParam(defaultValue = "timestamp") String sort,
|
||||
@RequestParam(defaultValue = "desc") String order,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@@ -46,8 +50,8 @@ public class AuditLogController {
|
||||
|
||||
size = Math.min(size, 100);
|
||||
|
||||
Instant fromInstant = from != null ? from.atStartOfDay(ZoneOffset.UTC).toInstant() : null;
|
||||
Instant toInstant = to != null ? to.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant() : null;
|
||||
Instant fromInstant = from != null ? from : Instant.now().minus(java.time.Duration.ofDays(7));
|
||||
Instant toInstant = to != null ? to : Instant.now();
|
||||
|
||||
AuditCategory cat = null;
|
||||
if (category != null && !category.isEmpty()) {
|
||||
@@ -58,6 +62,8 @@ public class AuditLogController {
|
||||
}
|
||||
}
|
||||
|
||||
auditService.log("view_audit_log", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
|
||||
AuditQuery query = new AuditQuery(username, cat, search, fromInstant, toInstant, sort, order, page, size);
|
||||
AuditPage result = auditRepository.find(query);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.cameleer3.server.app.dto.TableSizeResponse;
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import com.zaxxer.hikari.HikariPoolMXBean;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/database")
|
||||
@@ -35,11 +38,14 @@ public class DatabaseAdminController {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final DataSource dataSource;
|
||||
private final AuditService auditService;
|
||||
private final IngestionService ingestionService;
|
||||
|
||||
public DatabaseAdminController(JdbcTemplate jdbc, DataSource dataSource, AuditService auditService) {
|
||||
public DatabaseAdminController(JdbcTemplate jdbc, DataSource dataSource,
|
||||
AuditService auditService, IngestionService ingestionService) {
|
||||
this.jdbc = jdbc;
|
||||
this.dataSource = dataSource;
|
||||
this.auditService = auditService;
|
||||
this.ingestionService = ingestionService;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@@ -53,7 +59,8 @@ public class DatabaseAdminController {
|
||||
String host = extractHost(dataSource);
|
||||
return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new DatabaseStatusResponse(false, null, null, null, false));
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new DatabaseStatusResponse(false, null, null, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +124,29 @@ public class DatabaseAdminController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/metrics-pipeline")
|
||||
@Operation(summary = "Get metrics ingestion pipeline diagnostics")
|
||||
public ResponseEntity<Map<String, Object>> getMetricsPipeline() {
|
||||
int bufferDepth = ingestionService.getMetricsBufferDepth();
|
||||
|
||||
Long totalRows = jdbc.queryForObject(
|
||||
"SELECT count(*) FROM agent_metrics", Long.class);
|
||||
List<String> agentIds = jdbc.queryForList(
|
||||
"SELECT DISTINCT agent_id FROM agent_metrics ORDER BY agent_id", String.class);
|
||||
Instant latestCollected = jdbc.queryForObject(
|
||||
"SELECT max(collected_at) FROM agent_metrics", Instant.class);
|
||||
List<String> metricNames = jdbc.queryForList(
|
||||
"SELECT DISTINCT metric_name FROM agent_metrics ORDER BY metric_name", String.class);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"bufferDepth", bufferDepth,
|
||||
"totalRows", totalRows != null ? totalRows : 0,
|
||||
"distinctAgents", agentIds,
|
||||
"distinctMetrics", metricNames,
|
||||
"latestCollectedAt", latestCollected != null ? latestCollected.toString() : "none"
|
||||
));
|
||||
}
|
||||
|
||||
private String extractHost(DataSource ds) {
|
||||
try {
|
||||
if (ds instanceof HikariDataSource hds) {
|
||||
|
||||
@@ -49,7 +49,7 @@ public class DetailController {
|
||||
}
|
||||
|
||||
@GetMapping("/{executionId}/processors/{index}/snapshot")
|
||||
@Operation(summary = "Get exchange snapshot for a specific processor")
|
||||
@Operation(summary = "Get exchange snapshot for a specific processor by index")
|
||||
@ApiResponse(responseCode = "200", description = "Snapshot data")
|
||||
@ApiResponse(responseCode = "404", description = "Snapshot not found")
|
||||
public ResponseEntity<Map<String, String>> getProcessorSnapshot(
|
||||
@@ -69,4 +69,16 @@ public class DetailController {
|
||||
|
||||
return ResponseEntity.ok(snapshot);
|
||||
}
|
||||
|
||||
@GetMapping("/{executionId}/processors/by-id/{processorId}/snapshot")
|
||||
@Operation(summary = "Get exchange snapshot for a specific processor by processorId")
|
||||
@ApiResponse(responseCode = "200", description = "Snapshot data")
|
||||
@ApiResponse(responseCode = "404", description = "Snapshot not found")
|
||||
public ResponseEntity<Map<String, String>> processorSnapshotById(
|
||||
@PathVariable String executionId,
|
||||
@PathVariable String processorId) {
|
||||
return detailService.getProcessorSnapshot(executionId, processorId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.common.graph.RouteGraph;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.ingestion.IngestionService;
|
||||
import com.cameleer3.server.core.ingestion.TaggedDiagram;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -35,10 +37,14 @@ public class DiagramController {
|
||||
private static final Logger log = LoggerFactory.getLogger(DiagramController.class);
|
||||
|
||||
private final IngestionService ingestionService;
|
||||
private final AgentRegistryService registryService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DiagramController(IngestionService ingestionService, ObjectMapper objectMapper) {
|
||||
public DiagramController(IngestionService ingestionService,
|
||||
AgentRegistryService registryService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.ingestionService = ingestionService;
|
||||
this.registryService = registryService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -48,10 +54,11 @@ public class DiagramController {
|
||||
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
||||
public ResponseEntity<Void> ingestDiagrams(@RequestBody String body) throws JsonProcessingException {
|
||||
String agentId = extractAgentId();
|
||||
String applicationName = resolveApplicationName(agentId);
|
||||
List<RouteGraph> graphs = parsePayload(body);
|
||||
|
||||
for (RouteGraph graph : graphs) {
|
||||
ingestionService.ingestDiagram(new TaggedDiagram(agentId, graph));
|
||||
ingestionService.ingestDiagram(new TaggedDiagram(agentId, applicationName, graph));
|
||||
}
|
||||
|
||||
return ResponseEntity.accepted().build();
|
||||
@@ -62,6 +69,11 @@ public class DiagramController {
|
||||
return auth != null ? auth.getName() : "";
|
||||
}
|
||||
|
||||
private String resolveApplicationName(String agentId) {
|
||||
AgentInfo agent = registryService.findById(agentId);
|
||||
return agent != null ? agent.application() : "";
|
||||
}
|
||||
|
||||
private List<RouteGraph> parsePayload(String body) throws JsonProcessingException {
|
||||
String trimmed = body.strip();
|
||||
if (trimmed.startsWith("[")) {
|
||||
|
||||
@@ -62,6 +62,7 @@ public class DiagramRenderController {
|
||||
@ApiResponse(responseCode = "404", description = "Diagram not found")
|
||||
public ResponseEntity<?> renderDiagram(
|
||||
@PathVariable String contentHash,
|
||||
@RequestParam(defaultValue = "LR") String direction,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Optional<RouteGraph> graphOpt = diagramStore.findByContentHash(contentHash);
|
||||
@@ -76,7 +77,7 @@ public class DiagramRenderController {
|
||||
// without also accepting everything (*/*). This means "application/json"
|
||||
// must appear and wildcards must not dominate the preference.
|
||||
if (accept != null && isJsonPreferred(accept)) {
|
||||
DiagramLayout layout = diagramRenderer.layoutJson(graph);
|
||||
DiagramLayout layout = diagramRenderer.layoutJson(graph, direction);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(layout);
|
||||
@@ -96,7 +97,8 @@ public class DiagramRenderController {
|
||||
@ApiResponse(responseCode = "404", description = "No diagram found for the given application and route")
|
||||
public ResponseEntity<DiagramLayout> findByApplicationAndRoute(
|
||||
@RequestParam String application,
|
||||
@RequestParam String routeId) {
|
||||
@RequestParam String routeId,
|
||||
@RequestParam(defaultValue = "LR") String direction) {
|
||||
List<String> agentIds = registryService.findByApplication(application).stream()
|
||||
.map(AgentInfo::id)
|
||||
.toList();
|
||||
@@ -115,7 +117,7 @@ public class DiagramRenderController {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get());
|
||||
DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get(), direction);
|
||||
return ResponseEntity.ok(layout);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.common.model.LogBatch;
|
||||
import com.cameleer3.server.app.search.OpenSearchLogIndex;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/data")
|
||||
@Tag(name = "Ingestion", description = "Data ingestion endpoints")
|
||||
public class LogIngestionController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LogIngestionController.class);
|
||||
|
||||
private final OpenSearchLogIndex logIndex;
|
||||
private final AgentRegistryService registryService;
|
||||
|
||||
public LogIngestionController(OpenSearchLogIndex logIndex,
|
||||
AgentRegistryService registryService) {
|
||||
this.logIndex = logIndex;
|
||||
this.registryService = registryService;
|
||||
}
|
||||
|
||||
@PostMapping("/logs")
|
||||
@Operation(summary = "Ingest application log entries",
|
||||
description = "Accepts a batch of log entries from an agent. Entries are indexed in OpenSearch.")
|
||||
@ApiResponse(responseCode = "202", description = "Logs accepted for indexing")
|
||||
public ResponseEntity<Void> ingestLogs(@RequestBody LogBatch batch) {
|
||||
String agentId = extractAgentId();
|
||||
String application = resolveApplicationName(agentId);
|
||||
|
||||
if (batch.getEntries() != null && !batch.getEntries().isEmpty()) {
|
||||
log.debug("Received {} log entries from agent={}, app={}", batch.getEntries().size(), agentId, application);
|
||||
logIndex.indexBatch(agentId, application, batch.getEntries());
|
||||
}
|
||||
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
private String extractAgentId() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
return auth != null ? auth.getName() : "";
|
||||
}
|
||||
|
||||
private String resolveApplicationName(String agentId) {
|
||||
AgentInfo agent = registryService.findById(agentId);
|
||||
return agent != null ? agent.application() : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.dto.LogEntryResponse;
|
||||
import com.cameleer3.server.app.search.OpenSearchLogIndex;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/logs")
|
||||
@Tag(name = "Application Logs", description = "Query application logs stored in OpenSearch")
|
||||
public class LogQueryController {
|
||||
|
||||
private final OpenSearchLogIndex logIndex;
|
||||
|
||||
public LogQueryController(OpenSearchLogIndex logIndex) {
|
||||
this.logIndex = logIndex;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Search application log entries",
|
||||
description = "Returns log entries for a given application, optionally filtered by agent, level, time range, and text query")
|
||||
public ResponseEntity<List<LogEntryResponse>> searchLogs(
|
||||
@RequestParam String application,
|
||||
@RequestParam(required = false) String agentId,
|
||||
@RequestParam(required = false) String level,
|
||||
@RequestParam(required = false) String query,
|
||||
@RequestParam(required = false) String exchangeId,
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to,
|
||||
@RequestParam(defaultValue = "200") int limit) {
|
||||
|
||||
limit = Math.min(limit, 1000);
|
||||
|
||||
Instant fromInstant = from != null ? Instant.parse(from) : null;
|
||||
Instant toInstant = to != null ? Instant.parse(to) : null;
|
||||
|
||||
List<LogEntryResponse> entries = logIndex.search(
|
||||
application, agentId, level, query, exchangeId, fromInstant, toInstant, limit);
|
||||
|
||||
return ResponseEntity.ok(entries);
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,23 @@ public class MetricsController {
|
||||
@Operation(summary = "Ingest agent metrics",
|
||||
description = "Accepts an array of MetricsSnapshot objects")
|
||||
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
||||
@ApiResponse(responseCode = "400", description = "Invalid payload")
|
||||
@ApiResponse(responseCode = "503", description = "Buffer full, retry later")
|
||||
public ResponseEntity<Void> ingestMetrics(@RequestBody String body) throws JsonProcessingException {
|
||||
List<MetricsSnapshot> metrics = parsePayload(body);
|
||||
boolean accepted = ingestionService.acceptMetrics(metrics);
|
||||
public ResponseEntity<Void> ingestMetrics(@RequestBody String body) {
|
||||
List<MetricsSnapshot> metrics;
|
||||
try {
|
||||
metrics = parsePayload(body);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to parse metrics payload: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
log.debug("Received {} metric(s) from agent(s)", metrics.size());
|
||||
|
||||
boolean accepted = ingestionService.acceptMetrics(metrics);
|
||||
if (!accepted) {
|
||||
log.warn("Metrics buffer full, returning 503");
|
||||
log.warn("Metrics buffer full ({} items), returning 503",
|
||||
ingestionService.getMetricsBufferDepth());
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.header("Retry-After", "5")
|
||||
.build();
|
||||
|
||||
@@ -61,7 +61,8 @@ public class OidcConfigAdminController {
|
||||
@GetMapping
|
||||
@Operation(summary = "Get OIDC configuration")
|
||||
@ApiResponse(responseCode = "200", description = "Current OIDC configuration (client_secret masked)")
|
||||
public ResponseEntity<OidcAdminConfigResponse> getConfig() {
|
||||
public ResponseEntity<OidcAdminConfigResponse> getConfig(HttpServletRequest httpRequest) {
|
||||
auditService.log("view_oidc_config", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
Optional<OidcConfig> config = configRepository.find();
|
||||
if (config.isEmpty()) {
|
||||
return ResponseEntity.ok(OidcAdminConfigResponse.unconfigured());
|
||||
|
||||
@@ -49,12 +49,14 @@ public class OpenSearchAdminController {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String opensearchUrl;
|
||||
private final String indexPrefix;
|
||||
private final String logIndexPrefix;
|
||||
|
||||
public OpenSearchAdminController(OpenSearchClient client, RestClient restClient,
|
||||
SearchIndexerStats indexerStats, AuditService auditService,
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${opensearch.url:http://localhost:9200}") String opensearchUrl,
|
||||
@Value("${opensearch.index-prefix:executions-}") String indexPrefix) {
|
||||
@Value("${opensearch.index-prefix:executions-}") String indexPrefix,
|
||||
@Value("${opensearch.log-index-prefix:logs-}") String logIndexPrefix) {
|
||||
this.client = client;
|
||||
this.restClient = restClient;
|
||||
this.indexerStats = indexerStats;
|
||||
@@ -62,6 +64,7 @@ public class OpenSearchAdminController {
|
||||
this.objectMapper = objectMapper;
|
||||
this.opensearchUrl = opensearchUrl;
|
||||
this.indexPrefix = indexPrefix;
|
||||
this.logIndexPrefix = logIndexPrefix;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@@ -77,7 +80,8 @@ public class OpenSearchAdminController {
|
||||
health.numberOfNodes(),
|
||||
opensearchUrl));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new OpenSearchStatusResponse(
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new OpenSearchStatusResponse(
|
||||
false, "UNREACHABLE", null, 0, opensearchUrl));
|
||||
}
|
||||
}
|
||||
@@ -100,7 +104,8 @@ public class OpenSearchAdminController {
|
||||
public ResponseEntity<IndicesPageResponse> getIndices(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(defaultValue = "") String search) {
|
||||
@RequestParam(defaultValue = "") String search,
|
||||
@RequestParam(defaultValue = "executions") String prefix) {
|
||||
try {
|
||||
Response response = restClient.performRequest(
|
||||
new Request("GET", "/_cat/indices?format=json&h=index,health,docs.count,store.size,pri,rep&bytes=b"));
|
||||
@@ -109,10 +114,12 @@ public class OpenSearchAdminController {
|
||||
indices = objectMapper.readTree(is);
|
||||
}
|
||||
|
||||
String filterPrefix = "logs".equals(prefix) ? logIndexPrefix : indexPrefix;
|
||||
|
||||
List<IndexInfoResponse> allIndices = new ArrayList<>();
|
||||
for (JsonNode idx : indices) {
|
||||
String name = idx.path("index").asText("");
|
||||
if (!name.startsWith(indexPrefix)) {
|
||||
if (!name.startsWith(filterPrefix)) {
|
||||
continue;
|
||||
}
|
||||
if (!search.isEmpty() && !name.contains(search)) {
|
||||
@@ -143,7 +150,8 @@ public class OpenSearchAdminController {
|
||||
pageItems, totalIndices, totalDocs,
|
||||
humanSize(totalBytes), page, size, totalPages));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new IndicesPageResponse(
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(new IndicesPageResponse(
|
||||
List.of(), 0, 0, "0 B", page, size, 0));
|
||||
}
|
||||
}
|
||||
@@ -152,7 +160,7 @@ public class OpenSearchAdminController {
|
||||
@Operation(summary = "Delete an OpenSearch index")
|
||||
public ResponseEntity<Void> deleteIndex(@PathVariable String name, HttpServletRequest request) {
|
||||
try {
|
||||
if (!name.startsWith(indexPrefix)) {
|
||||
if (!name.startsWith(indexPrefix) && !name.startsWith(logIndexPrefix)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete index outside application scope");
|
||||
}
|
||||
boolean exists = client.indices().exists(r -> r.index(name)).value();
|
||||
@@ -228,7 +236,8 @@ public class OpenSearchAdminController {
|
||||
searchLatency, indexingLatency,
|
||||
heapUsed, heapMax));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(new PerformanceResponse(0, 0, 0, 0, 0, 0));
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(new PerformanceResponse(0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.cameleer3.server.app.controller;
|
||||
import com.cameleer3.server.app.dto.AgentSummary;
|
||||
import com.cameleer3.server.app.dto.AppCatalogEntry;
|
||||
import com.cameleer3.server.app.dto.RouteSummary;
|
||||
import com.cameleer3.common.graph.RouteGraph;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.agent.AgentState;
|
||||
import com.cameleer3.server.core.storage.DiagramStore;
|
||||
import com.cameleer3.server.core.storage.StatsStore;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -14,6 +16,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
@@ -33,10 +36,14 @@ import java.util.stream.Collectors;
|
||||
public class RouteCatalogController {
|
||||
|
||||
private final AgentRegistryService registryService;
|
||||
private final DiagramStore diagramStore;
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
public RouteCatalogController(AgentRegistryService registryService, JdbcTemplate jdbc) {
|
||||
public RouteCatalogController(AgentRegistryService registryService,
|
||||
DiagramStore diagramStore,
|
||||
JdbcTemplate jdbc) {
|
||||
this.registryService = registryService;
|
||||
this.diagramStore = diagramStore;
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
@@ -44,7 +51,9 @@ public class RouteCatalogController {
|
||||
@Operation(summary = "Get route catalog",
|
||||
description = "Returns all applications with their routes, agents, and health status")
|
||||
@ApiResponse(responseCode = "200", description = "Catalog returned")
|
||||
public ResponseEntity<List<AppCatalogEntry>> getCatalog() {
|
||||
public ResponseEntity<List<AppCatalogEntry>> getCatalog(
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to) {
|
||||
List<AgentInfo> allAgents = registryService.findAll();
|
||||
|
||||
// Group agents by application name
|
||||
@@ -63,9 +72,10 @@ public class RouteCatalogController {
|
||||
routesByApp.put(entry.getKey(), routes);
|
||||
}
|
||||
|
||||
// Query route-level stats for the last 24 hours
|
||||
// Time range for exchange counts — use provided range or default to last 24h
|
||||
Instant now = Instant.now();
|
||||
Instant from24h = now.minus(24, ChronoUnit.HOURS);
|
||||
Instant rangeFrom = from != null ? Instant.parse(from) : now.minus(24, ChronoUnit.HOURS);
|
||||
Instant rangeTo = to != null ? Instant.parse(to) : now;
|
||||
Instant from1m = now.minus(1, ChronoUnit.MINUTES);
|
||||
|
||||
// Route exchange counts from continuous aggregate
|
||||
@@ -82,7 +92,7 @@ public class RouteCatalogController {
|
||||
Timestamp ts = rs.getTimestamp("last_seen");
|
||||
if (ts != null) routeLastSeen.put(key, ts.toInstant());
|
||||
},
|
||||
Timestamp.from(from24h), Timestamp.from(now));
|
||||
Timestamp.from(rangeFrom), Timestamp.from(rangeTo));
|
||||
} catch (Exception e) {
|
||||
// Continuous aggregate may not exist yet
|
||||
}
|
||||
@@ -110,12 +120,14 @@ public class RouteCatalogController {
|
||||
|
||||
// Routes
|
||||
Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
|
||||
List<String> agentIds = agents.stream().map(AgentInfo::id).toList();
|
||||
List<RouteSummary> routeSummaries = routeIds.stream()
|
||||
.map(routeId -> {
|
||||
String key = appId + "/" + routeId;
|
||||
long count = routeExchangeCounts.getOrDefault(key, 0L);
|
||||
Instant lastSeen = routeLastSeen.get(key);
|
||||
return new RouteSummary(routeId, count, lastSeen);
|
||||
String fromUri = resolveFromEndpointUri(routeId, agentIds);
|
||||
return new RouteSummary(routeId, count, lastSeen, fromUri);
|
||||
})
|
||||
.toList();
|
||||
|
||||
@@ -137,6 +149,15 @@ public class RouteCatalogController {
|
||||
return ResponseEntity.ok(catalog);
|
||||
}
|
||||
|
||||
/** Resolve the from() endpoint URI for a route by looking up its diagram. */
|
||||
private String resolveFromEndpointUri(String routeId, List<String> agentIds) {
|
||||
return diagramStore.findContentHashForRouteByAgents(routeId, agentIds)
|
||||
.flatMap(diagramStore::findByContentHash)
|
||||
.map(RouteGraph::getRoot)
|
||||
.map(root -> root.getEndpointUri())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String computeWorstHealth(List<AgentInfo> agents) {
|
||||
boolean hasDead = false;
|
||||
boolean hasStale = false;
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.app.dto.ProcessorMetrics;
|
||||
import com.cameleer3.server.app.dto.RouteMetrics;
|
||||
import com.cameleer3.server.core.admin.AppSettings;
|
||||
import com.cameleer3.server.core.admin.AppSettingsRepository;
|
||||
import com.cameleer3.server.core.storage.StatsStore;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -18,6 +21,7 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/routes")
|
||||
@@ -25,9 +29,14 @@ import java.util.List;
|
||||
public class RouteMetricsController {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final StatsStore statsStore;
|
||||
private final AppSettingsRepository appSettingsRepository;
|
||||
|
||||
public RouteMetricsController(JdbcTemplate jdbc) {
|
||||
public RouteMetricsController(JdbcTemplate jdbc, StatsStore statsStore,
|
||||
AppSettingsRepository appSettingsRepository) {
|
||||
this.jdbc = jdbc;
|
||||
this.statsStore = statsStore;
|
||||
this.appSettingsRepository = appSettingsRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/metrics")
|
||||
@@ -78,7 +87,7 @@ public class RouteMetricsController {
|
||||
|
||||
routeKeys.add(new RouteKey(applicationName, routeId));
|
||||
return new RouteMetrics(routeId, applicationName, total, successRate,
|
||||
avgDur, p99Dur, errorRate, tps, List.of());
|
||||
avgDur, p99Dur, errorRate, tps, List.of(), -1.0);
|
||||
}, params.toArray());
|
||||
|
||||
// Fetch sparklines (12 buckets over the time window)
|
||||
@@ -100,13 +109,34 @@ public class RouteMetricsController {
|
||||
m.appId(), m.routeId());
|
||||
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
|
||||
m.successRate(), m.avgDurationMs(), m.p99DurationMs(),
|
||||
m.errorRate(), m.throughputPerSec(), sparkline));
|
||||
m.errorRate(), m.throughputPerSec(), sparkline, m.slaCompliance()));
|
||||
} catch (Exception e) {
|
||||
// Leave sparkline empty on error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich with SLA compliance per route
|
||||
if (!metrics.isEmpty()) {
|
||||
// Determine SLA threshold (per-app or default)
|
||||
String effectiveAppId = appId != null ? appId : (metrics.isEmpty() ? null : metrics.get(0).appId());
|
||||
int threshold = appSettingsRepository.findByAppId(effectiveAppId != null ? effectiveAppId : "")
|
||||
.map(AppSettings::slaThresholdMs).orElse(300);
|
||||
|
||||
Map<String, long[]> slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant,
|
||||
effectiveAppId, threshold);
|
||||
|
||||
for (int i = 0; i < metrics.size(); i++) {
|
||||
RouteMetrics m = metrics.get(i);
|
||||
long[] counts = slaCounts.get(m.routeId());
|
||||
double sla = (counts != null && counts[1] > 0)
|
||||
? counts[0] * 100.0 / counts[1] : 100.0;
|
||||
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
|
||||
m.successRate(), m.avgDurationMs(), m.p99DurationMs(),
|
||||
m.errorRate(), m.throughputPerSec(), m.sparkline(), sla));
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(metrics);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.cameleer3.server.app.controller;
|
||||
|
||||
import com.cameleer3.server.core.admin.AppSettings;
|
||||
import com.cameleer3.server.core.admin.AppSettingsRepository;
|
||||
import com.cameleer3.server.core.agent.AgentInfo;
|
||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||
import com.cameleer3.server.core.search.ExecutionStats;
|
||||
@@ -8,6 +10,8 @@ import com.cameleer3.server.core.search.SearchRequest;
|
||||
import com.cameleer3.server.core.search.SearchResult;
|
||||
import com.cameleer3.server.core.search.SearchService;
|
||||
import com.cameleer3.server.core.search.StatsTimeseries;
|
||||
import com.cameleer3.server.core.search.TopError;
|
||||
import com.cameleer3.server.core.storage.StatsStore;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -20,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Search endpoints for querying route executions.
|
||||
@@ -34,10 +39,13 @@ public class SearchController {
|
||||
|
||||
private final SearchService searchService;
|
||||
private final AgentRegistryService registryService;
|
||||
private final AppSettingsRepository appSettingsRepository;
|
||||
|
||||
public SearchController(SearchService searchService, AgentRegistryService registryService) {
|
||||
public SearchController(SearchService searchService, AgentRegistryService registryService,
|
||||
AppSettingsRepository appSettingsRepository) {
|
||||
this.searchService = searchService;
|
||||
this.registryService = registryService;
|
||||
this.appSettingsRepository = appSettingsRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/executions")
|
||||
@@ -87,21 +95,29 @@ public class SearchController {
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@Operation(summary = "Aggregate execution stats (P99 latency, active count)")
|
||||
@Operation(summary = "Aggregate execution stats (P99 latency, active count, SLA compliance)")
|
||||
public ResponseEntity<ExecutionStats> stats(
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String application) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
ExecutionStats stats;
|
||||
if (routeId == null && application == null) {
|
||||
return ResponseEntity.ok(searchService.stats(from, end));
|
||||
}
|
||||
if (routeId == null) {
|
||||
return ResponseEntity.ok(searchService.statsForApp(from, end, application));
|
||||
}
|
||||
stats = searchService.stats(from, end);
|
||||
} else if (routeId == null) {
|
||||
stats = searchService.statsForApp(from, end, application);
|
||||
} else {
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
return ResponseEntity.ok(searchService.stats(from, end, routeId, agentIds));
|
||||
stats = searchService.stats(from, end, routeId, agentIds);
|
||||
}
|
||||
|
||||
// Enrich with SLA compliance
|
||||
int threshold = appSettingsRepository
|
||||
.findByAppId(application != null ? application : "")
|
||||
.map(AppSettings::slaThresholdMs).orElse(300);
|
||||
double sla = searchService.slaCompliance(from, end, threshold, application, routeId);
|
||||
return ResponseEntity.ok(stats.withSlaCompliance(sla));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/timeseries")
|
||||
@@ -126,6 +142,48 @@ public class SearchController {
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, routeId, agentIds));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/timeseries/by-app")
|
||||
@Operation(summary = "Timeseries grouped by application")
|
||||
public ResponseEntity<Map<String, StatsTimeseries>> timeseriesByApp(
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByApp(from, end, buckets));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/timeseries/by-route")
|
||||
@Operation(summary = "Timeseries grouped by route for an application")
|
||||
public ResponseEntity<Map<String, StatsTimeseries>> timeseriesByRoute(
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets,
|
||||
@RequestParam String application) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.timeseriesGroupedByRoute(from, end, buckets, application));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/punchcard")
|
||||
@Operation(summary = "Transaction punchcard: weekday x hour grid (rolling 7 days)")
|
||||
public ResponseEntity<List<StatsStore.PunchcardCell>> punchcard(
|
||||
@RequestParam(required = false) String application) {
|
||||
Instant to = Instant.now();
|
||||
Instant from = to.minus(java.time.Duration.ofDays(7));
|
||||
return ResponseEntity.ok(searchService.punchcard(from, to, application));
|
||||
}
|
||||
|
||||
@GetMapping("/errors/top")
|
||||
@Operation(summary = "Top N errors with velocity trend")
|
||||
public ResponseEntity<List<TopError>> topErrors(
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(defaultValue = "5") int limit) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
return ResponseEntity.ok(searchService.topErrors(from, end, application, routeId, limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an application name to agent IDs.
|
||||
* Returns null if application is null/blank (no filtering).
|
||||
|
||||
@@ -58,7 +58,8 @@ public class UserAdminController {
|
||||
@GetMapping
|
||||
@Operation(summary = "List all users with RBAC detail")
|
||||
@ApiResponse(responseCode = "200", description = "User list returned")
|
||||
public ResponseEntity<List<UserDetail>> listUsers() {
|
||||
public ResponseEntity<List<UserDetail>> listUsers(HttpServletRequest httpRequest) {
|
||||
auditService.log("view_users", AuditCategory.USER_MGMT, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(rbacService.listUsers());
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import com.cameleer3.server.core.admin.AppSettings;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "Per-application dashboard settings")
|
||||
public record AppSettingsRequest(
|
||||
@NotNull @Min(1)
|
||||
@Schema(description = "SLA duration threshold in milliseconds")
|
||||
Integer slaThresholdMs,
|
||||
|
||||
@NotNull @Min(0) @Max(100)
|
||||
@Schema(description = "Error rate % threshold for warning (yellow) health dot")
|
||||
Double healthErrorWarn,
|
||||
|
||||
@NotNull @Min(0) @Max(100)
|
||||
@Schema(description = "Error rate % threshold for critical (red) health dot")
|
||||
Double healthErrorCrit,
|
||||
|
||||
@NotNull @Min(0) @Max(100)
|
||||
@Schema(description = "SLA compliance % threshold for warning (yellow) health dot")
|
||||
Double healthSlaWarn,
|
||||
|
||||
@NotNull @Min(0) @Max(100)
|
||||
@Schema(description = "SLA compliance % threshold for critical (red) health dot")
|
||||
Double healthSlaCrit
|
||||
) {
|
||||
|
||||
public AppSettings toSettings(String appId) {
|
||||
Instant now = Instant.now();
|
||||
return new AppSettings(appId, slaThresholdMs, healthErrorWarn, healthErrorCrit,
|
||||
healthSlaWarn, healthSlaCrit, now, now);
|
||||
}
|
||||
|
||||
public List<String> validate() {
|
||||
List<String> errors = new ArrayList<>();
|
||||
if (healthErrorWarn != null && healthErrorCrit != null
|
||||
&& healthErrorWarn > healthErrorCrit) {
|
||||
errors.add("healthErrorWarn must be <= healthErrorCrit");
|
||||
}
|
||||
if (healthSlaWarn != null && healthSlaCrit != null
|
||||
&& healthSlaWarn < healthSlaCrit) {
|
||||
errors.add("healthSlaWarn must be >= healthSlaCrit (higher SLA = healthier)");
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
/**
|
||||
* Request body for command acknowledgment from agents.
|
||||
* Contains the result status and message of the command execution.
|
||||
*
|
||||
* @param status "SUCCESS" or "FAILURE"
|
||||
* @param message human-readable description of the result
|
||||
* @param data optional structured JSON data returned by the agent (e.g. expression evaluation results)
|
||||
*/
|
||||
public record CommandAckRequest(String status, String message, String data) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Application log entry from OpenSearch")
|
||||
public record LogEntryResponse(
|
||||
@Schema(description = "Log timestamp (ISO-8601)") String timestamp,
|
||||
@Schema(description = "Log level (INFO, WARN, ERROR, DEBUG)") String level,
|
||||
@Schema(description = "Logger name") String loggerName,
|
||||
@Schema(description = "Log message") String message,
|
||||
@Schema(description = "Thread name") String threadName,
|
||||
@Schema(description = "Stack trace (if present)") String stackTrace
|
||||
) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "Request to replay an exchange on an agent")
|
||||
public record ReplayRequest(
|
||||
@NotNull @Schema(description = "Camel route ID to replay on")
|
||||
String routeId,
|
||||
@Schema(description = "Message body for the replayed exchange")
|
||||
String body,
|
||||
@Schema(description = "Message headers for the replayed exchange")
|
||||
Map<String, String> headers,
|
||||
@Schema(description = "Exchange ID of the original execution being replayed (for audit trail)")
|
||||
String originalExchangeId
|
||||
) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Result of a replay command")
|
||||
public record ReplayResponse(
|
||||
@Schema(description = "Replay outcome: SUCCESS or FAILURE")
|
||||
String status,
|
||||
@Schema(description = "Human-readable result message")
|
||||
String message,
|
||||
@Schema(description = "Structured result data from the agent (JSON)")
|
||||
String data
|
||||
) {}
|
||||
@@ -15,5 +15,6 @@ public record RouteMetrics(
|
||||
@NotNull double p99DurationMs,
|
||||
@NotNull double errorRate,
|
||||
@NotNull double throughputPerSec,
|
||||
@NotNull List<Double> sparkline
|
||||
@NotNull List<Double> sparkline,
|
||||
double slaCompliance
|
||||
) {}
|
||||
|
||||
@@ -9,5 +9,7 @@ import java.time.Instant;
|
||||
public record RouteSummary(
|
||||
@NotNull String routeId,
|
||||
@NotNull long exchangeCount,
|
||||
Instant lastSeen
|
||||
Instant lastSeen,
|
||||
@Schema(description = "The from() endpoint URI, e.g. 'direct:processOrder'")
|
||||
String fromEndpointUri
|
||||
) {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
/**
|
||||
* Request body for testing a tap expression against sample data via a live agent.
|
||||
*
|
||||
* @param expression the expression to evaluate (e.g. Simple, JSONPath, XPath)
|
||||
* @param language the expression language identifier
|
||||
* @param body sample message body to evaluate the expression against
|
||||
* @param target what the expression targets (e.g. "body", "header", "property")
|
||||
*/
|
||||
public record TestExpressionRequest(String expression, String language, String body, String target) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cameleer3.server.app.dto;
|
||||
|
||||
/**
|
||||
* Response from testing a tap expression against sample data.
|
||||
*
|
||||
* @param result the evaluation result (null if an error occurred)
|
||||
* @param error error message if evaluation failed (null on success)
|
||||
*/
|
||||
public record TestExpressionResponse(String result, String error) {}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.cameleer3.server.app.interceptor;
|
||||
|
||||
import com.cameleer3.server.core.admin.AuditCategory;
|
||||
import com.cameleer3.server.core.admin.AuditResult;
|
||||
import com.cameleer3.server.core.admin.AuditService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Safety-net audit interceptor that logs a basic entry for any state-changing
|
||||
* request (POST/PUT/DELETE) that was not explicitly audited by the controller.
|
||||
* <p>
|
||||
* Controllers that call {@link AuditService#log} set the {@code audit.logged}
|
||||
* request attribute, which this interceptor checks to avoid double-recording.
|
||||
*/
|
||||
@Component
|
||||
public class AuditInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Set<String> AUDITABLE_METHODS = Set.of("POST", "PUT", "DELETE");
|
||||
private static final Set<String> EXCLUDED_PATHS = Set.of("/api/v1/search/executions");
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
public AuditInterceptor(AuditService auditService) {
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
if (!AUDITABLE_METHODS.contains(request.getMethod())) {
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(request.getAttribute("audit.logged"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
String path = request.getRequestURI();
|
||||
if (EXCLUDED_PATHS.contains(path)) {
|
||||
return;
|
||||
}
|
||||
AuditResult result = response.getStatus() < 400 ? AuditResult.SUCCESS : AuditResult.FAILURE;
|
||||
|
||||
auditService.log(
|
||||
"HTTP " + request.getMethod() + " " + path,
|
||||
AuditCategory.INFRA,
|
||||
path,
|
||||
Map.of("status", response.getStatus()),
|
||||
result,
|
||||
request);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import com.cameleer3.server.core.search.SearchResult;
|
||||
import com.cameleer3.server.core.storage.SearchIndex;
|
||||
import com.cameleer3.server.core.storage.model.ExecutionDocument;
|
||||
import com.cameleer3.server.core.storage.model.ExecutionDocument.ProcessorDoc;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.opensearch.client.json.JsonData;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
@@ -33,6 +35,8 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenSearchIndex.class);
|
||||
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
.withZone(ZoneOffset.UTC);
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
|
||||
|
||||
private final OpenSearchClient client;
|
||||
private final String indexPrefix;
|
||||
@@ -125,6 +129,12 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<String> HIGHLIGHT_FIELDS = List.of(
|
||||
"error_message", "attributes_text",
|
||||
"processors.input_body", "processors.output_body",
|
||||
"processors.input_headers", "processors.output_headers",
|
||||
"processors.attributes_text");
|
||||
|
||||
private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest(
|
||||
SearchRequest request, int size) {
|
||||
return org.opensearch.client.opensearch.core.SearchRequest.of(b -> {
|
||||
@@ -137,6 +147,17 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
.field(request.sortColumn())
|
||||
.order("asc".equalsIgnoreCase(request.sortDir())
|
||||
? SortOrder.Asc : SortOrder.Desc)));
|
||||
// Add highlight when full-text search is active
|
||||
if (request.text() != null && !request.text().isBlank()) {
|
||||
b.highlight(h -> {
|
||||
for (String field : HIGHLIGHT_FIELDS) {
|
||||
h.fields(field, hf -> hf
|
||||
.fragmentSize(120)
|
||||
.numberOfFragments(1));
|
||||
}
|
||||
return h;
|
||||
});
|
||||
}
|
||||
return b;
|
||||
});
|
||||
}
|
||||
@@ -158,14 +179,28 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
}
|
||||
|
||||
// Keyword filters (use .keyword sub-field for exact matching on dynamically mapped text fields)
|
||||
if (request.status() != null)
|
||||
filter.add(termQuery("status.keyword", request.status()));
|
||||
if (request.status() != null && !request.status().isBlank()) {
|
||||
String[] statuses = request.status().split(",");
|
||||
if (statuses.length == 1) {
|
||||
filter.add(termQuery("status.keyword", statuses[0].trim()));
|
||||
} else {
|
||||
filter.add(Query.of(q -> q.terms(t -> t
|
||||
.field("status.keyword")
|
||||
.terms(tv -> tv.value(
|
||||
java.util.Arrays.stream(statuses)
|
||||
.map(String::trim)
|
||||
.map(FieldValue::of)
|
||||
.toList())))));
|
||||
}
|
||||
}
|
||||
if (request.routeId() != null)
|
||||
filter.add(termQuery("route_id.keyword", request.routeId()));
|
||||
if (request.agentId() != null)
|
||||
filter.add(termQuery("agent_id.keyword", request.agentId()));
|
||||
if (request.correlationId() != null)
|
||||
filter.add(termQuery("correlation_id.keyword", request.correlationId()));
|
||||
if (request.application() != null && !request.application().isBlank())
|
||||
filter.add(termQuery("application_name.keyword", request.application()));
|
||||
|
||||
// Full-text search across all fields + nested processor fields
|
||||
if (request.text() != null && !request.text().isBlank()) {
|
||||
@@ -176,11 +211,13 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
// Search top-level text fields (analyzed match + wildcard for substring)
|
||||
textQueries.add(Query.of(q -> q.multiMatch(m -> m
|
||||
.query(text)
|
||||
.fields("error_message", "error_stacktrace"))));
|
||||
.fields("error_message", "error_stacktrace", "attributes_text"))));
|
||||
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||
.field("error_message").value(wildcard).caseInsensitive(true))));
|
||||
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||
.field("error_stacktrace").value(wildcard).caseInsensitive(true))));
|
||||
textQueries.add(Query.of(q -> q.wildcard(w -> w
|
||||
.field("attributes_text").value(wildcard).caseInsensitive(true))));
|
||||
|
||||
// Search nested processor fields (analyzed match + wildcard)
|
||||
textQueries.add(Query.of(q -> q.nested(n -> n
|
||||
@@ -189,14 +226,16 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
.query(text)
|
||||
.fields("processors.input_body", "processors.output_body",
|
||||
"processors.input_headers", "processors.output_headers",
|
||||
"processors.error_message", "processors.error_stacktrace"))))));
|
||||
"processors.error_message", "processors.error_stacktrace",
|
||||
"processors.attributes_text"))))));
|
||||
textQueries.add(Query.of(q -> q.nested(n -> n
|
||||
.path("processors")
|
||||
.query(nq -> nq.bool(nb -> nb.should(
|
||||
wildcardQuery("processors.input_body", wildcard),
|
||||
wildcardQuery("processors.output_body", wildcard),
|
||||
wildcardQuery("processors.input_headers", wildcard),
|
||||
wildcardQuery("processors.output_headers", wildcard)
|
||||
wildcardQuery("processors.output_headers", wildcard),
|
||||
wildcardQuery("processors.attributes_text", wildcard)
|
||||
).minimumShouldMatch("1"))))));
|
||||
|
||||
// Also try keyword fields for exact matches
|
||||
@@ -297,6 +336,11 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
map.put("duration_ms", doc.durationMs());
|
||||
map.put("error_message", doc.errorMessage());
|
||||
map.put("error_stacktrace", doc.errorStacktrace());
|
||||
if (doc.attributes() != null) {
|
||||
Map<String, String> attrs = parseAttributesJson(doc.attributes());
|
||||
map.put("attributes", attrs);
|
||||
map.put("attributes_text", flattenAttributes(attrs));
|
||||
}
|
||||
if (doc.processors() != null) {
|
||||
map.put("processors", doc.processors().stream().map(p -> {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
@@ -309,9 +353,16 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
pm.put("output_body", p.outputBody());
|
||||
pm.put("input_headers", p.inputHeaders());
|
||||
pm.put("output_headers", p.outputHeaders());
|
||||
if (p.attributes() != null) {
|
||||
Map<String, String> pAttrs = parseAttributesJson(p.attributes());
|
||||
pm.put("attributes", pAttrs);
|
||||
pm.put("attributes_text", flattenAttributes(pAttrs));
|
||||
}
|
||||
return pm;
|
||||
}).toList());
|
||||
}
|
||||
map.put("has_trace_data", doc.hasTraceData());
|
||||
map.put("is_replay", doc.isReplay());
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -319,6 +370,22 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
private ExecutionSummary hitToSummary(Hit<Map> hit) {
|
||||
Map<String, Object> src = hit.source();
|
||||
if (src == null) return null;
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> attributes = src.get("attributes") instanceof Map
|
||||
? new LinkedHashMap<>((Map<String, String>) src.get("attributes")) : null;
|
||||
// Merge processor-level attributes (execution-level takes precedence)
|
||||
if (src.get("processors") instanceof List<?> procs) {
|
||||
for (Object pObj : procs) {
|
||||
if (pObj instanceof Map<?, ?> pm && pm.get("attributes") instanceof Map<?, ?> pa) {
|
||||
if (attributes == null) attributes = new LinkedHashMap<>();
|
||||
for (var entry : pa.entrySet()) {
|
||||
attributes.putIfAbsent(
|
||||
String.valueOf(entry.getKey()),
|
||||
String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ExecutionSummary(
|
||||
(String) src.get("execution_id"),
|
||||
(String) src.get("route_id"),
|
||||
@@ -330,7 +397,37 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
src.get("duration_ms") != null ? ((Number) src.get("duration_ms")).longValue() : 0L,
|
||||
(String) src.get("correlation_id"),
|
||||
(String) src.get("error_message"),
|
||||
null // diagramContentHash not stored in index
|
||||
null, // diagramContentHash not stored in index
|
||||
extractHighlight(hit),
|
||||
attributes,
|
||||
Boolean.TRUE.equals(src.get("has_trace_data")),
|
||||
Boolean.TRUE.equals(src.get("is_replay"))
|
||||
);
|
||||
}
|
||||
|
||||
private String extractHighlight(Hit<Map> hit) {
|
||||
if (hit.highlight() == null || hit.highlight().isEmpty()) return null;
|
||||
for (List<String> fragments : hit.highlight().values()) {
|
||||
if (fragments != null && !fragments.isEmpty()) {
|
||||
return fragments.get(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseAttributesJson(String json) {
|
||||
if (json == null || json.isBlank()) return null;
|
||||
try {
|
||||
return JSON.readValue(json, STR_MAP);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String flattenAttributes(Map<String, String> attrs) {
|
||||
if (attrs == null || attrs.isEmpty()) return "";
|
||||
return attrs.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.cameleer3.server.app.search;
|
||||
|
||||
import com.cameleer3.common.model.LogEntry;
|
||||
import com.cameleer3.server.app.dto.LogEntryResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.opensearch.client.json.JsonData;
|
||||
import org.opensearch.client.opensearch.OpenSearchClient;
|
||||
import org.opensearch.client.opensearch._types.FieldValue;
|
||||
import org.opensearch.client.opensearch._types.SortOrder;
|
||||
import org.opensearch.client.opensearch._types.mapping.Property;
|
||||
import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
|
||||
import org.opensearch.client.opensearch._types.query_dsl.Query;
|
||||
import org.opensearch.client.opensearch.core.BulkRequest;
|
||||
import org.opensearch.client.opensearch.core.BulkResponse;
|
||||
import org.opensearch.client.opensearch.core.bulk.BulkResponseItem;
|
||||
import org.opensearch.client.opensearch.indices.ExistsIndexTemplateRequest;
|
||||
import org.opensearch.client.opensearch.indices.PutIndexTemplateRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class OpenSearchLogIndex {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenSearchLogIndex.class);
|
||||
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
.withZone(ZoneOffset.UTC);
|
||||
|
||||
private final OpenSearchClient client;
|
||||
private final String indexPrefix;
|
||||
private final int retentionDays;
|
||||
|
||||
public OpenSearchLogIndex(OpenSearchClient client,
|
||||
@Value("${opensearch.log-index-prefix:logs-}") String indexPrefix,
|
||||
@Value("${opensearch.log-retention-days:7}") int retentionDays) {
|
||||
this.client = client;
|
||||
this.indexPrefix = indexPrefix;
|
||||
this.retentionDays = retentionDays;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
ensureIndexTemplate();
|
||||
ensureIsmPolicy();
|
||||
}
|
||||
|
||||
private void ensureIndexTemplate() {
|
||||
String templateName = indexPrefix.replace("-", "") + "-template";
|
||||
String indexPattern = indexPrefix + "*";
|
||||
try {
|
||||
boolean exists = client.indices().existsIndexTemplate(
|
||||
ExistsIndexTemplateRequest.of(b -> b.name(templateName))).value();
|
||||
if (!exists) {
|
||||
client.indices().putIndexTemplate(PutIndexTemplateRequest.of(b -> b
|
||||
.name(templateName)
|
||||
.indexPatterns(List.of(indexPattern))
|
||||
.template(t -> t
|
||||
.settings(s -> s
|
||||
.numberOfShards("1")
|
||||
.numberOfReplicas("1"))
|
||||
.mappings(m -> m
|
||||
.properties("@timestamp", Property.of(p -> p.date(d -> d)))
|
||||
.properties("level", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("loggerName", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("message", Property.of(p -> p.text(tx -> tx)))
|
||||
.properties("threadName", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("stackTrace", Property.of(p -> p.text(tx -> tx)))
|
||||
.properties("agentId", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("application", Property.of(p -> p.keyword(k -> k)))
|
||||
.properties("exchangeId", Property.of(p -> p.keyword(k -> k)))))));
|
||||
log.info("OpenSearch log index template '{}' created", templateName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to create log index template", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureIsmPolicy() {
|
||||
String policyId = "logs-retention";
|
||||
try {
|
||||
// Use the low-level REST client to manage ISM policies
|
||||
var restClient = client._transport();
|
||||
// Check if the ISM policy exists via a GET; create if not
|
||||
// ISM is managed via the _plugins/_ism/policies API
|
||||
// For now, log a reminder — ISM policy should be created via OpenSearch API or dashboard
|
||||
log.info("Log retention policy: indices matching '{}*' should be deleted after {} days. " +
|
||||
"Ensure ISM policy '{}' is configured in OpenSearch.", indexPrefix, retentionDays, policyId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not verify ISM policy for log retention", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<LogEntryResponse> search(String application, String agentId, String level,
|
||||
String query, String exchangeId,
|
||||
Instant from, Instant to, int limit) {
|
||||
try {
|
||||
BoolQuery.Builder bool = new BoolQuery.Builder();
|
||||
bool.must(Query.of(q -> q.term(t -> t.field("application").value(FieldValue.of(application)))));
|
||||
if (agentId != null && !agentId.isEmpty()) {
|
||||
bool.must(Query.of(q -> q.term(t -> t.field("agentId").value(FieldValue.of(agentId)))));
|
||||
}
|
||||
if (exchangeId != null && !exchangeId.isEmpty()) {
|
||||
// Match on top-level field (new records) or MDC nested field (old records)
|
||||
bool.must(Query.of(q -> q.bool(b -> b
|
||||
.should(Query.of(s -> s.term(t -> t.field("exchangeId.keyword").value(FieldValue.of(exchangeId)))))
|
||||
.should(Query.of(s -> s.term(t -> t.field("mdc.camel.exchangeId.keyword").value(FieldValue.of(exchangeId)))))
|
||||
.minimumShouldMatch("1"))));
|
||||
}
|
||||
if (level != null && !level.isEmpty()) {
|
||||
bool.must(Query.of(q -> q.term(t -> t.field("level").value(FieldValue.of(level.toUpperCase())))));
|
||||
}
|
||||
if (query != null && !query.isEmpty()) {
|
||||
bool.must(Query.of(q -> q.match(m -> m.field("message").query(FieldValue.of(query)))));
|
||||
}
|
||||
if (from != null || to != null) {
|
||||
bool.must(Query.of(q -> q.range(r -> {
|
||||
r.field("@timestamp");
|
||||
if (from != null) r.gte(JsonData.of(from.toString()));
|
||||
if (to != null) r.lte(JsonData.of(to.toString()));
|
||||
return r;
|
||||
})));
|
||||
}
|
||||
|
||||
var response = client.search(s -> s
|
||||
.index(indexPrefix + "*")
|
||||
.query(Query.of(q -> q.bool(bool.build())))
|
||||
.sort(so -> so.field(f -> f.field("@timestamp").order(SortOrder.Desc)))
|
||||
.size(limit), Map.class);
|
||||
|
||||
List<LogEntryResponse> results = new ArrayList<>();
|
||||
for (var hit : response.hits().hits()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> src = (Map<String, Object>) hit.source();
|
||||
if (src == null) continue;
|
||||
results.add(new LogEntryResponse(
|
||||
str(src, "@timestamp"),
|
||||
str(src, "level"),
|
||||
str(src, "loggerName"),
|
||||
str(src, "message"),
|
||||
str(src, "threadName"),
|
||||
str(src, "stackTrace")));
|
||||
}
|
||||
return results;
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to search log entries for application={}", application, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static String str(Map<String, Object> map, String key) {
|
||||
Object v = map.get(key);
|
||||
return v != null ? v.toString() : null;
|
||||
}
|
||||
|
||||
public void indexBatch(String agentId, String application, List<LogEntry> entries) {
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
|
||||
|
||||
for (LogEntry entry : entries) {
|
||||
String indexName = indexPrefix + DAY_FMT.format(
|
||||
entry.getTimestamp() != null ? entry.getTimestamp() : java.time.Instant.now());
|
||||
|
||||
Map<String, Object> doc = toMap(entry, agentId, application);
|
||||
|
||||
bulkBuilder.operations(op -> op
|
||||
.index(idx -> idx
|
||||
.index(indexName)
|
||||
.document(doc)));
|
||||
}
|
||||
|
||||
BulkResponse response = client.bulk(bulkBuilder.build());
|
||||
|
||||
if (response.errors()) {
|
||||
int errorCount = 0;
|
||||
for (BulkResponseItem item : response.items()) {
|
||||
if (item.error() != null) {
|
||||
errorCount++;
|
||||
if (errorCount == 1) {
|
||||
log.error("Bulk log index error: {}", item.error().reason());
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("Bulk log indexing had {} error(s) out of {} entries", errorCount, entries.size());
|
||||
} else {
|
||||
log.debug("Indexed {} log entries for agent={}, app={}", entries.size(), agentId, application);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to bulk index {} log entries for agent={}", entries.size(), agentId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(LogEntry entry, String agentId, String application) {
|
||||
Map<String, Object> doc = new LinkedHashMap<>();
|
||||
doc.put("@timestamp", entry.getTimestamp() != null ? entry.getTimestamp().toString() : null);
|
||||
doc.put("level", entry.getLevel());
|
||||
doc.put("loggerName", entry.getLoggerName());
|
||||
doc.put("message", entry.getMessage());
|
||||
doc.put("threadName", entry.getThreadName());
|
||||
doc.put("stackTrace", entry.getStackTrace());
|
||||
doc.put("mdc", entry.getMdc());
|
||||
doc.put("agentId", agentId);
|
||||
doc.put("application", application);
|
||||
if (entry.getMdc() != null) {
|
||||
String exId = entry.getMdc().get("camel.exchangeId");
|
||||
if (exId != null) doc.put("exchangeId", exId);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,9 @@ public class OidcAuthController {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("OIDC callback failed: {}", e.getMessage(), e);
|
||||
auditService.log("unknown", "login_oidc", AuditCategory.AUTH, null,
|
||||
Map.of("reason", e.getMessage() != null ? e.getMessage() : "unknown"),
|
||||
AuditResult.FAILURE, httpRequest);
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
|
||||
"OIDC authentication failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -72,11 +72,16 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1/agents/*/commands").hasAnyRole("OPERATOR", "ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1/agents/groups/*/commands").hasAnyRole("OPERATOR", "ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1/agents/commands").hasAnyRole("OPERATOR", "ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1/agents/*/replay").hasAnyRole("OPERATOR", "ADMIN")
|
||||
|
||||
// Search endpoints
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
|
||||
.requestMatchers(HttpMethod.POST, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||
|
||||
// Application config endpoints
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/config/*").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
|
||||
.requestMatchers(HttpMethod.PUT, "/api/v1/config/*").hasAnyRole("OPERATOR", "ADMIN")
|
||||
|
||||
// Read-only data endpoints — viewer+
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||
.requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
|
||||
|
||||
@@ -123,7 +123,8 @@ public class UiAuthController {
|
||||
@ApiResponse(responseCode = "200", description = "Token refreshed")
|
||||
@ApiResponse(responseCode = "401", description = "Invalid refresh token",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request) {
|
||||
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
try {
|
||||
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
|
||||
if (!result.subject().startsWith("user:")) {
|
||||
@@ -138,6 +139,7 @@ public class UiAuthController {
|
||||
String displayName = userRepository.findById(result.subject())
|
||||
.map(UserInfo::displayName)
|
||||
.orElse(result.subject());
|
||||
auditService.log(result.subject(), "token_refresh", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
|
||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null));
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.cameleer3.server.app.storage;
|
||||
|
||||
import com.cameleer3.server.core.admin.AppSettings;
|
||||
import com.cameleer3.server.core.admin.AppSettingsRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class PostgresAppSettingsRepository implements AppSettingsRepository {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
private static final RowMapper<AppSettings> ROW_MAPPER = (rs, rowNum) -> new AppSettings(
|
||||
rs.getString("app_id"),
|
||||
rs.getInt("sla_threshold_ms"),
|
||||
rs.getDouble("health_error_warn"),
|
||||
rs.getDouble("health_error_crit"),
|
||||
rs.getDouble("health_sla_warn"),
|
||||
rs.getDouble("health_sla_crit"),
|
||||
rs.getTimestamp("created_at").toInstant(),
|
||||
rs.getTimestamp("updated_at").toInstant());
|
||||
|
||||
public PostgresAppSettingsRepository(JdbcTemplate jdbc) {
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AppSettings> findByAppId(String appId) {
|
||||
List<AppSettings> results = jdbc.query(
|
||||
"SELECT * FROM app_settings WHERE app_id = ?", ROW_MAPPER, appId);
|
||||
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppSettings> findAll() {
|
||||
return jdbc.query("SELECT * FROM app_settings ORDER BY app_id", ROW_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppSettings save(AppSettings settings) {
|
||||
jdbc.update("""
|
||||
INSERT INTO app_settings (app_id, sla_threshold_ms, health_error_warn,
|
||||
health_error_crit, health_sla_warn, health_sla_crit, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, now(), now())
|
||||
ON CONFLICT (app_id) DO UPDATE SET
|
||||
sla_threshold_ms = EXCLUDED.sla_threshold_ms,
|
||||
health_error_warn = EXCLUDED.health_error_warn,
|
||||
health_error_crit = EXCLUDED.health_error_crit,
|
||||
health_sla_warn = EXCLUDED.health_sla_warn,
|
||||
health_sla_crit = EXCLUDED.health_sla_crit,
|
||||
updated_at = now()
|
||||
""",
|
||||
settings.appId(), settings.slaThresholdMs(),
|
||||
settings.healthErrorWarn(), settings.healthErrorCrit(),
|
||||
settings.healthSlaWarn(), settings.healthSlaCrit());
|
||||
return findByAppId(settings.appId()).orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String appId) {
|
||||
jdbc.update("DELETE FROM app_settings WHERE app_id = ?", appId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.cameleer3.server.app.storage;
|
||||
|
||||
import com.cameleer3.common.model.ApplicationConfig;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class PostgresApplicationConfigRepository {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PostgresApplicationConfigRepository(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public List<ApplicationConfig> findAll() {
|
||||
return jdbc.query(
|
||||
"SELECT config_val, version, updated_at FROM application_config ORDER BY application",
|
||||
(rs, rowNum) -> {
|
||||
try {
|
||||
ApplicationConfig cfg = objectMapper.readValue(rs.getString("config_val"), ApplicationConfig.class);
|
||||
cfg.setVersion(rs.getInt("version"));
|
||||
cfg.setUpdatedAt(rs.getTimestamp("updated_at").toInstant());
|
||||
return cfg;
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to deserialize application config", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<ApplicationConfig> findByApplication(String application) {
|
||||
List<ApplicationConfig> results = jdbc.query(
|
||||
"SELECT config_val, version, updated_at FROM application_config WHERE application = ?",
|
||||
(rs, rowNum) -> {
|
||||
try {
|
||||
ApplicationConfig cfg = objectMapper.readValue(rs.getString("config_val"), ApplicationConfig.class);
|
||||
cfg.setVersion(rs.getInt("version"));
|
||||
cfg.setUpdatedAt(rs.getTimestamp("updated_at").toInstant());
|
||||
return cfg;
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to deserialize application config", e);
|
||||
}
|
||||
},
|
||||
application);
|
||||
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||||
}
|
||||
|
||||
public ApplicationConfig save(String application, ApplicationConfig config, String updatedBy) {
|
||||
String json;
|
||||
try {
|
||||
json = objectMapper.writeValueAsString(config);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to serialize application config", e);
|
||||
}
|
||||
|
||||
// Upsert: insert or update, auto-increment version
|
||||
int updated = jdbc.update("""
|
||||
INSERT INTO application_config (application, config_val, version, updated_at, updated_by)
|
||||
VALUES (?, ?::jsonb, 1, now(), ?)
|
||||
ON CONFLICT (application) DO UPDATE SET
|
||||
config_val = EXCLUDED.config_val,
|
||||
version = application_config.version + 1,
|
||||
updated_at = now(),
|
||||
updated_by = EXCLUDED.updated_by
|
||||
""",
|
||||
application, json, updatedBy);
|
||||
|
||||
return findByApplication(application).orElseThrow();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -33,8 +34,8 @@ public class PostgresDiagramStore implements DiagramStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(PostgresDiagramStore.class);
|
||||
|
||||
private static final String INSERT_SQL = """
|
||||
INSERT INTO route_diagrams (content_hash, route_id, agent_id, definition)
|
||||
VALUES (?, ?, ?, ?::jsonb)
|
||||
INSERT INTO route_diagrams (content_hash, route_id, agent_id, application_name, definition)
|
||||
VALUES (?, ?, ?, ?, ?::jsonb)
|
||||
ON CONFLICT (content_hash) DO NOTHING
|
||||
""";
|
||||
|
||||
@@ -62,11 +63,12 @@ public class PostgresDiagramStore implements DiagramStore {
|
||||
try {
|
||||
RouteGraph graph = diagram.graph();
|
||||
String agentId = diagram.agentId() != null ? diagram.agentId() : "";
|
||||
String applicationName = diagram.applicationName() != null ? diagram.applicationName() : "";
|
||||
String json = objectMapper.writeValueAsString(graph);
|
||||
String contentHash = sha256Hex(json);
|
||||
String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
|
||||
|
||||
jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, json);
|
||||
jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, applicationName, json);
|
||||
log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
|
||||
@@ -116,6 +118,21 @@ public class PostgresDiagramStore implements DiagramStore {
|
||||
return Optional.of((String) rows.get(0).get("content_hash"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> findProcessorRouteMapping(String applicationName) {
|
||||
Map<String, String> mapping = new HashMap<>();
|
||||
jdbcTemplate.query("""
|
||||
SELECT DISTINCT rd.route_id, node_elem->>'id' AS processor_id
|
||||
FROM route_diagrams rd,
|
||||
jsonb_array_elements(rd.definition::jsonb->'nodes') AS node_elem
|
||||
WHERE rd.application_name = ?
|
||||
AND node_elem->>'id' IS NOT NULL
|
||||
""",
|
||||
rs -> { mapping.put(rs.getString("processor_id"), rs.getString("route_id")); },
|
||||
applicationName);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
static String sha256Hex(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
@@ -27,8 +27,14 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
INSERT INTO executions (execution_id, route_id, agent_id, application_name,
|
||||
status, correlation_id, exchange_id, start_time, end_time,
|
||||
duration_ms, error_message, error_stacktrace, diagram_content_hash,
|
||||
engine_level, input_body, output_body, input_headers, output_headers,
|
||||
attributes,
|
||||
error_type, error_category, root_cause_type, root_cause_message,
|
||||
trace_id, span_id,
|
||||
processors_json, has_trace_data, is_replay,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb,
|
||||
?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, now(), now())
|
||||
ON CONFLICT (execution_id, start_time) DO UPDATE SET
|
||||
status = CASE
|
||||
WHEN EXCLUDED.status IN ('COMPLETED', 'FAILED')
|
||||
@@ -42,6 +48,21 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
error_message = COALESCE(EXCLUDED.error_message, executions.error_message),
|
||||
error_stacktrace = COALESCE(EXCLUDED.error_stacktrace, executions.error_stacktrace),
|
||||
diagram_content_hash = COALESCE(EXCLUDED.diagram_content_hash, executions.diagram_content_hash),
|
||||
engine_level = COALESCE(EXCLUDED.engine_level, executions.engine_level),
|
||||
input_body = COALESCE(EXCLUDED.input_body, executions.input_body),
|
||||
output_body = COALESCE(EXCLUDED.output_body, executions.output_body),
|
||||
input_headers = COALESCE(EXCLUDED.input_headers, executions.input_headers),
|
||||
output_headers = COALESCE(EXCLUDED.output_headers, executions.output_headers),
|
||||
attributes = COALESCE(EXCLUDED.attributes, executions.attributes),
|
||||
error_type = COALESCE(EXCLUDED.error_type, executions.error_type),
|
||||
error_category = COALESCE(EXCLUDED.error_category, executions.error_category),
|
||||
root_cause_type = COALESCE(EXCLUDED.root_cause_type, executions.root_cause_type),
|
||||
root_cause_message = COALESCE(EXCLUDED.root_cause_message, executions.root_cause_message),
|
||||
trace_id = COALESCE(EXCLUDED.trace_id, executions.trace_id),
|
||||
span_id = COALESCE(EXCLUDED.span_id, executions.span_id),
|
||||
processors_json = COALESCE(EXCLUDED.processors_json, executions.processors_json),
|
||||
has_trace_data = EXCLUDED.has_trace_data OR executions.has_trace_data,
|
||||
is_replay = EXCLUDED.is_replay OR executions.is_replay,
|
||||
updated_at = now()
|
||||
""",
|
||||
execution.executionId(), execution.routeId(), execution.agentId(),
|
||||
@@ -50,7 +71,15 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
Timestamp.from(execution.startTime()),
|
||||
execution.endTime() != null ? Timestamp.from(execution.endTime()) : null,
|
||||
execution.durationMs(), execution.errorMessage(),
|
||||
execution.errorStacktrace(), execution.diagramContentHash());
|
||||
execution.errorStacktrace(), execution.diagramContentHash(),
|
||||
execution.engineLevel(),
|
||||
execution.inputBody(), execution.outputBody(),
|
||||
execution.inputHeaders(), execution.outputHeaders(),
|
||||
execution.attributes(),
|
||||
execution.errorType(), execution.errorCategory(),
|
||||
execution.rootCauseType(), execution.rootCauseMessage(),
|
||||
execution.traceId(), execution.spanId(),
|
||||
execution.processorsJson(), execution.hasTraceData(), execution.isReplay());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,10 +88,15 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
List<ProcessorRecord> processors) {
|
||||
jdbc.batchUpdate("""
|
||||
INSERT INTO processor_executions (execution_id, processor_id, processor_type,
|
||||
diagram_node_id, application_name, route_id, depth, parent_processor_id,
|
||||
application_name, route_id, depth, parent_processor_id,
|
||||
status, start_time, end_time, duration_ms, error_message, error_stacktrace,
|
||||
input_body, output_body, input_headers, output_headers)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb)
|
||||
input_body, output_body, input_headers, output_headers, attributes,
|
||||
loop_index, loop_size, split_index, split_size, multicast_index,
|
||||
resolved_endpoint_uri,
|
||||
error_type, error_category, root_cause_type, root_cause_message,
|
||||
error_handler_type, circuit_breaker_state, fallback_triggered)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (execution_id, processor_id, start_time) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
end_time = COALESCE(EXCLUDED.end_time, processor_executions.end_time),
|
||||
@@ -72,16 +106,38 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
input_body = COALESCE(EXCLUDED.input_body, processor_executions.input_body),
|
||||
output_body = COALESCE(EXCLUDED.output_body, processor_executions.output_body),
|
||||
input_headers = COALESCE(EXCLUDED.input_headers, processor_executions.input_headers),
|
||||
output_headers = COALESCE(EXCLUDED.output_headers, processor_executions.output_headers)
|
||||
output_headers = COALESCE(EXCLUDED.output_headers, processor_executions.output_headers),
|
||||
attributes = COALESCE(EXCLUDED.attributes, processor_executions.attributes),
|
||||
loop_index = COALESCE(EXCLUDED.loop_index, processor_executions.loop_index),
|
||||
loop_size = COALESCE(EXCLUDED.loop_size, processor_executions.loop_size),
|
||||
split_index = COALESCE(EXCLUDED.split_index, processor_executions.split_index),
|
||||
split_size = COALESCE(EXCLUDED.split_size, processor_executions.split_size),
|
||||
multicast_index = COALESCE(EXCLUDED.multicast_index, processor_executions.multicast_index),
|
||||
resolved_endpoint_uri = COALESCE(EXCLUDED.resolved_endpoint_uri, processor_executions.resolved_endpoint_uri),
|
||||
error_type = COALESCE(EXCLUDED.error_type, processor_executions.error_type),
|
||||
error_category = COALESCE(EXCLUDED.error_category, processor_executions.error_category),
|
||||
root_cause_type = COALESCE(EXCLUDED.root_cause_type, processor_executions.root_cause_type),
|
||||
root_cause_message = COALESCE(EXCLUDED.root_cause_message, processor_executions.root_cause_message),
|
||||
error_handler_type = COALESCE(EXCLUDED.error_handler_type, processor_executions.error_handler_type),
|
||||
circuit_breaker_state = COALESCE(EXCLUDED.circuit_breaker_state, processor_executions.circuit_breaker_state),
|
||||
fallback_triggered = COALESCE(EXCLUDED.fallback_triggered, processor_executions.fallback_triggered)
|
||||
""",
|
||||
processors.stream().map(p -> new Object[]{
|
||||
p.executionId(), p.processorId(), p.processorType(),
|
||||
p.diagramNodeId(), p.applicationName(), p.routeId(),
|
||||
p.applicationName(), p.routeId(),
|
||||
p.depth(), p.parentProcessorId(), p.status(),
|
||||
Timestamp.from(p.startTime()),
|
||||
p.endTime() != null ? Timestamp.from(p.endTime()) : null,
|
||||
p.durationMs(), p.errorMessage(), p.errorStacktrace(),
|
||||
p.inputBody(), p.outputBody(), p.inputHeaders(), p.outputHeaders()
|
||||
p.inputBody(), p.outputBody(), p.inputHeaders(), p.outputHeaders(),
|
||||
p.attributes(),
|
||||
p.loopIndex(), p.loopSize(), p.splitIndex(), p.splitSize(),
|
||||
p.multicastIndex(),
|
||||
p.resolvedEndpointUri(),
|
||||
p.errorType(), p.errorCategory(),
|
||||
p.rootCauseType(), p.rootCauseMessage(),
|
||||
p.errorHandlerType(), p.circuitBreakerState(),
|
||||
p.fallbackTriggered()
|
||||
}).toList());
|
||||
}
|
||||
|
||||
@@ -100,6 +156,13 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
PROCESSOR_MAPPER, executionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProcessorRecord> findProcessorById(String executionId, String processorId) {
|
||||
String sql = "SELECT * FROM processor_executions WHERE execution_id = ? AND processor_id = ? LIMIT 1";
|
||||
List<ProcessorRecord> results = jdbc.query(sql, PROCESSOR_MAPPER, executionId, processorId);
|
||||
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||||
}
|
||||
|
||||
private static final RowMapper<ExecutionRecord> EXECUTION_MAPPER = (rs, rowNum) ->
|
||||
new ExecutionRecord(
|
||||
rs.getString("execution_id"), rs.getString("route_id"),
|
||||
@@ -109,12 +172,22 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
toInstant(rs, "start_time"), toInstant(rs, "end_time"),
|
||||
rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
|
||||
rs.getString("error_message"), rs.getString("error_stacktrace"),
|
||||
rs.getString("diagram_content_hash"));
|
||||
rs.getString("diagram_content_hash"),
|
||||
rs.getString("engine_level"),
|
||||
rs.getString("input_body"), rs.getString("output_body"),
|
||||
rs.getString("input_headers"), rs.getString("output_headers"),
|
||||
rs.getString("attributes"),
|
||||
rs.getString("error_type"), rs.getString("error_category"),
|
||||
rs.getString("root_cause_type"), rs.getString("root_cause_message"),
|
||||
rs.getString("trace_id"), rs.getString("span_id"),
|
||||
rs.getString("processors_json"),
|
||||
rs.getBoolean("has_trace_data"),
|
||||
rs.getBoolean("is_replay"));
|
||||
|
||||
private static final RowMapper<ProcessorRecord> PROCESSOR_MAPPER = (rs, rowNum) ->
|
||||
new ProcessorRecord(
|
||||
rs.getString("execution_id"), rs.getString("processor_id"),
|
||||
rs.getString("processor_type"), rs.getString("diagram_node_id"),
|
||||
rs.getString("processor_type"),
|
||||
rs.getString("application_name"), rs.getString("route_id"),
|
||||
rs.getInt("depth"), rs.getString("parent_processor_id"),
|
||||
rs.getString("status"),
|
||||
@@ -122,7 +195,18 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
|
||||
rs.getString("error_message"), rs.getString("error_stacktrace"),
|
||||
rs.getString("input_body"), rs.getString("output_body"),
|
||||
rs.getString("input_headers"), rs.getString("output_headers"));
|
||||
rs.getString("input_headers"), rs.getString("output_headers"),
|
||||
rs.getString("attributes"),
|
||||
rs.getObject("loop_index") != null ? rs.getInt("loop_index") : null,
|
||||
rs.getObject("loop_size") != null ? rs.getInt("loop_size") : null,
|
||||
rs.getObject("split_index") != null ? rs.getInt("split_index") : null,
|
||||
rs.getObject("split_size") != null ? rs.getInt("split_size") : null,
|
||||
rs.getObject("multicast_index") != null ? rs.getInt("multicast_index") : null,
|
||||
rs.getString("resolved_endpoint_uri"),
|
||||
rs.getString("error_type"), rs.getString("error_category"),
|
||||
rs.getString("root_cause_type"), rs.getString("root_cause_message"),
|
||||
rs.getString("error_handler_type"), rs.getString("circuit_breaker_state"),
|
||||
rs.getObject("fallback_triggered") != null ? rs.getBoolean("fallback_triggered") : null);
|
||||
|
||||
private static Instant toInstant(ResultSet rs, String column) throws SQLException {
|
||||
Timestamp ts = rs.getTimestamp(column);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cameleer3.server.app.storage;
|
||||
import com.cameleer3.server.core.search.ExecutionStats;
|
||||
import com.cameleer3.server.core.search.StatsTimeseries;
|
||||
import com.cameleer3.server.core.search.StatsTimeseries.TimeseriesBucket;
|
||||
import com.cameleer3.server.core.search.TopError;
|
||||
import com.cameleer3.server.core.storage.StatsStore;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -12,7 +13,9 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class PostgresStatsStore implements StatsStore {
|
||||
@@ -184,4 +187,242 @@ public class PostgresStatsStore implements StatsStore {
|
||||
|
||||
return new StatsTimeseries(buckets);
|
||||
}
|
||||
|
||||
// ── Grouped timeseries ────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount) {
|
||||
return queryGroupedTimeseries("stats_1m_app", "application_name", from, to,
|
||||
bucketCount, List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, StatsTimeseries> timeseriesGroupedByRoute(Instant from, Instant to,
|
||||
int bucketCount, String applicationName) {
|
||||
return queryGroupedTimeseries("stats_1m_route", "route_id", from, to,
|
||||
bucketCount, List.of(new Filter("application_name", applicationName)));
|
||||
}
|
||||
|
||||
private Map<String, StatsTimeseries> queryGroupedTimeseries(
|
||||
String view, String groupCol, Instant from, Instant to,
|
||||
int bucketCount, List<Filter> filters) {
|
||||
|
||||
long intervalSeconds = Duration.between(from, to).toSeconds() / Math.max(bucketCount, 1);
|
||||
if (intervalSeconds < 60) intervalSeconds = 60;
|
||||
|
||||
String sql = "SELECT time_bucket(? * INTERVAL '1 second', bucket) AS period, " +
|
||||
groupCol + " AS group_key, " +
|
||||
"COALESCE(SUM(total_count), 0) AS total_count, " +
|
||||
"COALESCE(SUM(failed_count), 0) AS failed_count, " +
|
||||
"CASE WHEN SUM(total_count) > 0 THEN SUM(duration_sum) / SUM(total_count) ELSE 0 END AS avg_duration, " +
|
||||
"COALESCE(MAX(p99_duration), 0) AS p99_duration, " +
|
||||
"COALESCE(SUM(running_count), 0) AS active_count " +
|
||||
"FROM " + view + " WHERE bucket >= ? AND bucket < ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(intervalSeconds);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
for (Filter f : filters) {
|
||||
sql += " AND " + f.column() + " = ?";
|
||||
params.add(f.value());
|
||||
}
|
||||
sql += " GROUP BY period, group_key ORDER BY period, group_key";
|
||||
|
||||
Map<String, List<TimeseriesBucket>> grouped = new LinkedHashMap<>();
|
||||
jdbc.query(sql, (rs) -> {
|
||||
String key = rs.getString("group_key");
|
||||
TimeseriesBucket bucket = new TimeseriesBucket(
|
||||
rs.getTimestamp("period").toInstant(),
|
||||
rs.getLong("total_count"), rs.getLong("failed_count"),
|
||||
rs.getLong("avg_duration"), rs.getLong("p99_duration"),
|
||||
rs.getLong("active_count"));
|
||||
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(bucket);
|
||||
}, params.toArray());
|
||||
|
||||
Map<String, StatsTimeseries> result = new LinkedHashMap<>();
|
||||
grouped.forEach((key, buckets) -> result.put(key, new StatsTimeseries(buckets)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── SLA compliance ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public double slaCompliance(Instant from, Instant to, int thresholdMs,
|
||||
String applicationName, String routeId) {
|
||||
String sql = "SELECT " +
|
||||
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
|
||||
"FROM executions WHERE start_time >= ? AND start_time < ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(thresholdMs);
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (applicationName != null) {
|
||||
sql += " AND application_name = ?";
|
||||
params.add(applicationName);
|
||||
}
|
||||
if (routeId != null) {
|
||||
sql += " AND route_id = ?";
|
||||
params.add(routeId);
|
||||
}
|
||||
|
||||
return jdbc.query(sql, (rs, rowNum) -> {
|
||||
long total = rs.getLong("total");
|
||||
if (total == 0) return 1.0;
|
||||
return rs.getLong("compliant") * 100.0 / total;
|
||||
}, params.toArray()).stream().findFirst().orElse(1.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs) {
|
||||
String sql = "SELECT application_name, " +
|
||||
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
|
||||
"FROM executions WHERE start_time >= ? AND start_time < ? " +
|
||||
"GROUP BY application_name";
|
||||
|
||||
Map<String, long[]> result = new LinkedHashMap<>();
|
||||
jdbc.query(sql, (rs) -> {
|
||||
result.put(rs.getString("application_name"),
|
||||
new long[]{rs.getLong("compliant"), rs.getLong("total")});
|
||||
}, defaultThresholdMs, Timestamp.from(from), Timestamp.from(to));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, long[]> slaCountsByRoute(Instant from, Instant to,
|
||||
String applicationName, int thresholdMs) {
|
||||
String sql = "SELECT route_id, " +
|
||||
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
|
||||
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
|
||||
"FROM executions WHERE start_time >= ? AND start_time < ? " +
|
||||
"AND application_name = ? GROUP BY route_id";
|
||||
|
||||
Map<String, long[]> result = new LinkedHashMap<>();
|
||||
jdbc.query(sql, (rs) -> {
|
||||
result.put(rs.getString("route_id"),
|
||||
new long[]{rs.getLong("compliant"), rs.getLong("total")});
|
||||
}, thresholdMs, Timestamp.from(from), Timestamp.from(to), applicationName);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Top errors ────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public List<TopError> topErrors(Instant from, Instant to, String applicationName,
|
||||
String routeId, int limit) {
|
||||
StringBuilder where = new StringBuilder(
|
||||
"status = 'FAILED' AND start_time >= ? AND start_time < ?");
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (applicationName != null) {
|
||||
where.append(" AND application_name = ?");
|
||||
params.add(applicationName);
|
||||
}
|
||||
|
||||
String table;
|
||||
String groupId;
|
||||
if (routeId != null) {
|
||||
// L3: attribute errors to processors
|
||||
table = "processor_executions";
|
||||
groupId = "processor_id";
|
||||
where.append(" AND route_id = ?");
|
||||
params.add(routeId);
|
||||
} else {
|
||||
// L1/L2: attribute errors to routes
|
||||
table = "executions";
|
||||
groupId = "route_id";
|
||||
}
|
||||
|
||||
Instant fiveMinAgo = Instant.now().minus(5, ChronoUnit.MINUTES);
|
||||
Instant tenMinAgo = Instant.now().minus(10, ChronoUnit.MINUTES);
|
||||
|
||||
String sql = "WITH counted AS (" +
|
||||
" SELECT COALESCE(error_type, LEFT(error_message, 200)) AS error_key, " +
|
||||
" " + groupId + " AS group_id, " +
|
||||
" COUNT(*) AS cnt, MAX(start_time) AS last_seen " +
|
||||
" FROM " + table + " WHERE " + where +
|
||||
" GROUP BY error_key, group_id ORDER BY cnt DESC LIMIT ?" +
|
||||
"), velocity AS (" +
|
||||
" SELECT COALESCE(error_type, LEFT(error_message, 200)) AS error_key, " +
|
||||
" COUNT(*) FILTER (WHERE start_time >= ?) AS recent_5m, " +
|
||||
" COUNT(*) FILTER (WHERE start_time >= ? AND start_time < ?) AS prev_5m " +
|
||||
" FROM " + table + " WHERE " + where +
|
||||
" GROUP BY error_key" +
|
||||
") SELECT c.error_key, c.group_id, c.cnt, c.last_seen, " +
|
||||
" COALESCE(v.recent_5m, 0) / 5.0 AS velocity, " +
|
||||
" CASE " +
|
||||
" WHEN COALESCE(v.recent_5m, 0) > COALESCE(v.prev_5m, 0) * 1.2 THEN 'accelerating' " +
|
||||
" WHEN COALESCE(v.recent_5m, 0) < COALESCE(v.prev_5m, 0) * 0.8 THEN 'decelerating' " +
|
||||
" ELSE 'stable' END AS trend " +
|
||||
"FROM counted c LEFT JOIN velocity v ON c.error_key = v.error_key " +
|
||||
"ORDER BY c.cnt DESC";
|
||||
|
||||
// Build full params: counted-where params + limit + velocity timestamps + velocity-where params
|
||||
List<Object> fullParams = new ArrayList<>(params);
|
||||
fullParams.add(limit);
|
||||
fullParams.add(Timestamp.from(fiveMinAgo));
|
||||
fullParams.add(Timestamp.from(tenMinAgo));
|
||||
fullParams.add(Timestamp.from(fiveMinAgo));
|
||||
fullParams.addAll(params); // same where clause for velocity CTE
|
||||
|
||||
return jdbc.query(sql, (rs, rowNum) -> {
|
||||
String errorKey = rs.getString("error_key");
|
||||
String gid = rs.getString("group_id");
|
||||
return new TopError(
|
||||
errorKey,
|
||||
routeId != null ? routeId : gid, // routeId
|
||||
routeId != null ? gid : null, // processorId (only at L3)
|
||||
rs.getLong("cnt"),
|
||||
rs.getDouble("velocity"),
|
||||
rs.getString("trend"),
|
||||
rs.getTimestamp("last_seen").toInstant());
|
||||
}, fullParams.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int activeErrorTypes(Instant from, Instant to, String applicationName) {
|
||||
String sql = "SELECT COUNT(DISTINCT COALESCE(error_type, LEFT(error_message, 200))) " +
|
||||
"FROM executions WHERE status = 'FAILED' AND start_time >= ? AND start_time < ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (applicationName != null) {
|
||||
sql += " AND application_name = ?";
|
||||
params.add(applicationName);
|
||||
}
|
||||
|
||||
Integer count = jdbc.queryForObject(sql, Integer.class, params.toArray());
|
||||
return count != null ? count : 0;
|
||||
}
|
||||
|
||||
// ── Punchcard ─────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public List<PunchcardCell> punchcard(Instant from, Instant to, String applicationName) {
|
||||
String view = applicationName != null ? "stats_1m_app" : "stats_1m_all";
|
||||
String sql = "SELECT EXTRACT(DOW FROM bucket) AS weekday, " +
|
||||
"EXTRACT(HOUR FROM bucket) AS hour, " +
|
||||
"COALESCE(SUM(total_count), 0) AS total_count, " +
|
||||
"COALESCE(SUM(failed_count), 0) AS failed_count " +
|
||||
"FROM " + view + " WHERE bucket >= ? AND bucket < ?";
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(Timestamp.from(from));
|
||||
params.add(Timestamp.from(to));
|
||||
if (applicationName != null) {
|
||||
sql += " AND application_name = ?";
|
||||
params.add(applicationName);
|
||||
}
|
||||
sql += " GROUP BY weekday, hour ORDER BY weekday, hour";
|
||||
|
||||
return jdbc.query(sql, (rs, rowNum) -> new PunchcardCell(
|
||||
rs.getInt("weekday"), rs.getInt("hour"),
|
||||
rs.getLong("total_count"), rs.getLong("failed_count")),
|
||||
params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ public class SpaForwardController {
|
||||
@GetMapping(value = {
|
||||
"/login",
|
||||
"/executions",
|
||||
"/executions/{path:[^\\.]*}",
|
||||
"/executions/**",
|
||||
"/oidc/callback",
|
||||
"/admin/{path:[^\\.]*}"
|
||||
"/admin/**"
|
||||
})
|
||||
public String forward() {
|
||||
return "forward:/index.html";
|
||||
|
||||
@@ -42,6 +42,8 @@ opensearch:
|
||||
index-prefix: ${CAMELEER_OPENSEARCH_INDEX_PREFIX:executions-}
|
||||
queue-size: ${CAMELEER_OPENSEARCH_QUEUE_SIZE:10000}
|
||||
debounce-ms: ${CAMELEER_OPENSEARCH_DEBOUNCE_MS:2000}
|
||||
log-index-prefix: ${CAMELEER_LOG_INDEX_PREFIX:logs-}
|
||||
log-retention-days: ${CAMELEER_LOG_RETENTION_DAYS:7}
|
||||
|
||||
cameleer:
|
||||
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- executions: store raw processor tree for faithful detail response
|
||||
ALTER TABLE executions ADD COLUMN processors_json JSONB;
|
||||
|
||||
-- executions: error categorization + OTel tracing
|
||||
ALTER TABLE executions ADD COLUMN error_type TEXT;
|
||||
ALTER TABLE executions ADD COLUMN error_category TEXT;
|
||||
ALTER TABLE executions ADD COLUMN root_cause_type TEXT;
|
||||
ALTER TABLE executions ADD COLUMN root_cause_message TEXT;
|
||||
ALTER TABLE executions ADD COLUMN trace_id TEXT;
|
||||
ALTER TABLE executions ADD COLUMN span_id TEXT;
|
||||
|
||||
-- processor_executions: error categorization + circuit breaker
|
||||
ALTER TABLE processor_executions ADD COLUMN error_type TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN error_category TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN root_cause_type TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN root_cause_message TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN error_handler_type TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN circuit_breaker_state TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN fallback_triggered BOOLEAN;
|
||||
|
||||
-- Remove erroneous depth columns from V9
|
||||
ALTER TABLE processor_executions DROP COLUMN IF EXISTS split_depth;
|
||||
ALTER TABLE processor_executions DROP COLUMN IF EXISTS loop_depth;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Flag indicating whether any processor in this execution captured trace data
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS has_trace_data BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Backfill: set flag for existing executions that have processor trace data
|
||||
UPDATE executions e SET has_trace_data = TRUE
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM processor_executions pe
|
||||
WHERE pe.execution_id = e.execution_id
|
||||
AND (pe.input_body IS NOT NULL OR pe.output_body IS NOT NULL)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Per-application dashboard settings (SLA thresholds, health dot thresholds)
|
||||
CREATE TABLE app_settings (
|
||||
app_id TEXT PRIMARY KEY,
|
||||
sla_threshold_ms INTEGER NOT NULL DEFAULT 300,
|
||||
health_error_warn DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
||||
health_error_crit DOUBLE PRECISION NOT NULL DEFAULT 5.0,
|
||||
health_sla_warn DOUBLE PRECISION NOT NULL DEFAULT 99.0,
|
||||
health_sla_crit DOUBLE PRECISION NOT NULL DEFAULT 95.0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Flag indicating whether this execution is a replayed exchange
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS is_replay BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Backfill: check inputHeaders JSON for X-Cameleer-Replay header
|
||||
UPDATE executions SET is_replay = TRUE
|
||||
WHERE input_headers IS NOT NULL
|
||||
AND input_headers::jsonb ? 'X-Cameleer-Replay';
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add engine level and route-level snapshot columns to executions table.
|
||||
-- Required for REGULAR engine level where route-level payloads exist but
|
||||
-- no processor execution records are created.
|
||||
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS engine_level VARCHAR(16);
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS input_body TEXT;
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS output_body TEXT;
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS input_headers JSONB;
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS output_headers JSONB;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Per-application configuration for agent observability settings.
|
||||
-- Agents download this at startup and receive updates via SSE CONFIG_UPDATE.
|
||||
CREATE TABLE application_config (
|
||||
application TEXT PRIMARY KEY,
|
||||
config_val JSONB NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by TEXT
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE executions ADD COLUMN IF NOT EXISTS attributes JSONB;
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS attributes JSONB;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE processor_executions DROP COLUMN IF EXISTS diagram_node_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE route_diagrams ADD COLUMN IF NOT EXISTS application_name TEXT NOT NULL DEFAULT '';
|
||||
CREATE INDEX IF NOT EXISTS idx_diagrams_application ON route_diagrams (application_name);
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_index INTEGER;
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_size INTEGER;
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_index INTEGER;
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_size INTEGER;
|
||||
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS multicast_index INTEGER;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE processor_executions ADD COLUMN resolved_endpoint_uri TEXT;
|
||||
ALTER TABLE processor_executions ADD COLUMN split_depth INTEGER DEFAULT 0;
|
||||
ALTER TABLE processor_executions ADD COLUMN loop_depth INTEGER DEFAULT 0;
|
||||
@@ -50,11 +50,11 @@ class BackpressureIT extends AbstractPostgresIT {
|
||||
// Fill the metrics buffer completely with a batch of 5
|
||||
String batchJson = """
|
||||
[
|
||||
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:00Z","metrics":{}},
|
||||
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:01Z","metrics":{}},
|
||||
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:02Z","metrics":{}},
|
||||
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:03Z","metrics":{}},
|
||||
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:04Z","metrics":{}}
|
||||
{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:00Z","metricName":"test.metric","metricValue":1.0,"tags":{}},
|
||||
{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:01Z","metricName":"test.metric","metricValue":2.0,"tags":{}},
|
||||
{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:02Z","metricName":"test.metric","metricValue":3.0,"tags":{}},
|
||||
{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:03Z","metricName":"test.metric","metricValue":4.0,"tags":{}},
|
||||
{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:04Z","metricName":"test.metric","metricValue":5.0,"tags":{}}
|
||||
]
|
||||
""";
|
||||
|
||||
@@ -66,7 +66,7 @@ class BackpressureIT extends AbstractPostgresIT {
|
||||
|
||||
// Now buffer should be full -- next POST should get 503
|
||||
String overflowJson = """
|
||||
[{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:05Z","metrics":{}}]
|
||||
[{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:05Z","metricName":"test.metric","metricValue":6.0,"tags":{}}]
|
||||
""";
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(
|
||||
|
||||
@@ -65,7 +65,6 @@ class DetailControllerIT extends AbstractPostgresIT {
|
||||
"startTime": "2026-03-10T10:00:00Z",
|
||||
"endTime": "2026-03-10T10:00:01Z",
|
||||
"durationMs": 1000,
|
||||
"diagramNodeId": "node-root",
|
||||
"inputBody": "root-input-body",
|
||||
"outputBody": "root-output-body",
|
||||
"inputHeaders": {"Content-Type": "application/json"},
|
||||
@@ -78,7 +77,6 @@ class DetailControllerIT extends AbstractPostgresIT {
|
||||
"startTime": "2026-03-10T10:00:00.100Z",
|
||||
"endTime": "2026-03-10T10:00:00.200Z",
|
||||
"durationMs": 100,
|
||||
"diagramNodeId": "node-child1",
|
||||
"inputBody": "child1-input",
|
||||
"outputBody": "child1-output",
|
||||
"inputHeaders": {},
|
||||
@@ -91,7 +89,6 @@ class DetailControllerIT extends AbstractPostgresIT {
|
||||
"startTime": "2026-03-10T10:00:00.200Z",
|
||||
"endTime": "2026-03-10T10:00:00.800Z",
|
||||
"durationMs": 600,
|
||||
"diagramNodeId": "node-child2",
|
||||
"inputBody": "child2-input",
|
||||
"outputBody": "child2-output",
|
||||
"inputHeaders": {},
|
||||
@@ -104,7 +101,6 @@ class DetailControllerIT extends AbstractPostgresIT {
|
||||
"startTime": "2026-03-10T10:00:00.300Z",
|
||||
"endTime": "2026-03-10T10:00:00.700Z",
|
||||
"durationMs": 400,
|
||||
"diagramNodeId": "node-gc",
|
||||
"inputBody": "gc-input",
|
||||
"outputBody": "gc-output",
|
||||
"inputHeaders": {"X-GC": "true"},
|
||||
|
||||
@@ -39,8 +39,7 @@ class DiagramControllerIT extends AbstractPostgresIT {
|
||||
"description": "Test route",
|
||||
"version": 1,
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"processorNodeMapping": {}
|
||||
"edges": []
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -60,8 +59,7 @@ class DiagramControllerIT extends AbstractPostgresIT {
|
||||
"description": "Flush test",
|
||||
"version": 1,
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"processorNodeMapping": {}
|
||||
"edges": []
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@@ -53,8 +53,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
|
||||
"edges": [
|
||||
{"source": "n1", "target": "n2", "edgeType": "FLOW"},
|
||||
{"source": "n2", "target": "n3", "edgeType": "FLOW"}
|
||||
],
|
||||
"processorNodeMapping": {}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ class ElkDiagramRendererTest {
|
||||
RouteNode process = new RouteNode("node-2", NodeType.BEAN, "myProcessor");
|
||||
RouteNode to = new RouteNode("node-3", NodeType.TO, "log:output");
|
||||
|
||||
graph.setNodes(List.of(from, process, to));
|
||||
from.setChildren(List.of(process, to));
|
||||
graph.setRoot(from);
|
||||
graph.setEdges(List.of(
|
||||
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW)
|
||||
@@ -62,10 +63,10 @@ class ElkDiagramRendererTest {
|
||||
RouteNode otherwise = new RouteNode("node-4", NodeType.EIP_OTHERWISE, "otherwise");
|
||||
RouteNode to = new RouteNode("node-5", NodeType.TO, "log:result");
|
||||
|
||||
// Set children on the choice node
|
||||
// Build tree: from → [choice, to]; choice → [when, otherwise]
|
||||
choice.setChildren(List.of(when, otherwise));
|
||||
|
||||
graph.setNodes(List.of(from, choice, when, otherwise, to));
|
||||
from.setChildren(List.of(choice, to));
|
||||
graph.setRoot(from);
|
||||
graph.setEdges(List.of(
|
||||
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
|
||||
@@ -172,6 +173,97 @@ class ElkDiagramRendererTest {
|
||||
assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DO_TRY graph: from -> doTry(try: [process, log], doFinally: [cleanup], doCatch: [errorLog]) -> to
|
||||
*/
|
||||
private RouteGraph buildDoTryGraph() {
|
||||
RouteGraph graph = new RouteGraph("try-catch-route");
|
||||
graph.setExtractedAt(Instant.now());
|
||||
graph.setVersion(1);
|
||||
|
||||
RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
|
||||
RouteNode doTry = new RouteNode("node-2", NodeType.DO_TRY, "doTry");
|
||||
RouteNode process = new RouteNode("node-3", NodeType.PROCESSOR, "process");
|
||||
RouteNode log1 = new RouteNode("node-4", NodeType.LOG, "log:tryBody");
|
||||
RouteNode doFinally = new RouteNode("node-5", NodeType.DO_FINALLY, "doFinally");
|
||||
RouteNode cleanup = new RouteNode("node-6", NodeType.LOG, "log:cleanup");
|
||||
RouteNode doCatch = new RouteNode("node-7", NodeType.DO_CATCH, "doCatch");
|
||||
RouteNode errorLog = new RouteNode("node-8", NodeType.LOG, "log:error");
|
||||
RouteNode to = new RouteNode("node-9", NodeType.TO, "log:done");
|
||||
|
||||
doFinally.setChildren(List.of(cleanup));
|
||||
doCatch.setChildren(List.of(errorLog));
|
||||
doTry.setChildren(List.of(process, log1, doFinally, doCatch));
|
||||
from.setChildren(List.of(doTry, to));
|
||||
graph.setRoot(from);
|
||||
graph.setEdges(List.of(
|
||||
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-3", "node-4", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-5", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-5", "node-6", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-7", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-7", "node-8", RouteEdge.EdgeType.FLOW),
|
||||
new RouteEdge("node-2", "node-9", RouteEdge.EdgeType.FLOW)
|
||||
));
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutJson_doTryGraph_sectionsInCorrectOrder() {
|
||||
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||
|
||||
assertNotNull(layout);
|
||||
|
||||
// Find the DO_TRY compound node
|
||||
PositionedNode doTryNode = layout.nodes().stream()
|
||||
.filter(n -> "node-2".equals(n.id()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||
|
||||
assertNotNull(doTryNode.children(), "DO_TRY should have children");
|
||||
assertFalse(doTryNode.children().isEmpty(), "DO_TRY should have non-empty children");
|
||||
|
||||
// Find sections by ID pattern
|
||||
PositionedNode tryBody = doTryNode.children().stream()
|
||||
.filter(n -> n.id() != null && n.id().contains("._try_body"))
|
||||
.findFirst().orElse(null);
|
||||
PositionedNode finallySection = doTryNode.children().stream()
|
||||
.filter(n -> "node-5".equals(n.id()))
|
||||
.findFirst().orElse(null);
|
||||
PositionedNode catchSection = doTryNode.children().stream()
|
||||
.filter(n -> "node-7".equals(n.id()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
assertNotNull(tryBody, "Try body wrapper should exist");
|
||||
assertNotNull(finallySection, "doFinally section should exist");
|
||||
assertNotNull(catchSection, "doCatch section should exist");
|
||||
|
||||
// Verify vertical order: tryBody.y < doFinally.y < doCatch.y
|
||||
assertTrue(tryBody.y() < finallySection.y(),
|
||||
"Try body (y=" + tryBody.y() + ") should be above doFinally (y=" + finallySection.y() + ")");
|
||||
assertTrue(finallySection.y() < catchSection.y(),
|
||||
"doFinally (y=" + finallySection.y() + ") should be above doCatch (y=" + catchSection.y() + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
void layoutJson_doTryGraph_sectionsHaveSameWidth() {
|
||||
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
|
||||
|
||||
PositionedNode doTryNode = layout.nodes().stream()
|
||||
.filter(n -> "node-2".equals(n.id()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
|
||||
|
||||
List<PositionedNode> sections = doTryNode.children();
|
||||
double firstWidth = sections.get(0).width();
|
||||
for (PositionedNode section : sections) {
|
||||
assertEquals(firstWidth, section.width(), 0.1,
|
||||
"All sections should have the same width, but " + section.id() + " differs");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderSvg_compoundGraph_producesValidSvg() {
|
||||
String svg = renderer.renderSvg(buildCompoundGraph());
|
||||
|
||||
@@ -35,7 +35,8 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
|
||||
now, now.plusMillis(100), 100L,
|
||||
"OrderNotFoundException: order-12345 not found", null,
|
||||
List.of(new ProcessorDoc("proc-1", "log", "COMPLETED",
|
||||
null, null, "request body with customer-99", null, null, null)));
|
||||
null, null, "request body with customer-99", null, null, null, null)),
|
||||
null, false, false);
|
||||
|
||||
searchIndex.index(doc);
|
||||
refreshOpenSearchIndices();
|
||||
@@ -60,7 +61,8 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
|
||||
"COMPLETED", null, null,
|
||||
now, now.plusMillis(50), 50L, null, null,
|
||||
List.of(new ProcessorDoc("proc-1", "bean", "COMPLETED",
|
||||
null, null, "UniquePayloadIdentifier12345", null, null, null)));
|
||||
null, null, "UniquePayloadIdentifier12345", null, null, null, null)),
|
||||
null, false, false);
|
||||
|
||||
searchIndex.index(doc);
|
||||
refreshOpenSearchIndices();
|
||||
|
||||
@@ -46,8 +46,7 @@ class DiagramLinkingIT extends AbstractPostgresIT {
|
||||
],
|
||||
"edges": [
|
||||
{"source": "n1", "target": "n2", "edgeType": "FLOW"}
|
||||
],
|
||||
"processorNodeMapping": {}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user