diff --git a/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log b/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log
new file mode 100644
index 0000000..e6bad9e
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T06-25-14-829Z.log
@@ -0,0 +1,2 @@
+[ 19340ms] [ERROR] Failed to load resource: the server responded with a status of 403 () @ https://app2.hyeonworks.com/oauth2/callback?state=rKipZCUv8W5a-xgYheJbjBsInoD5Il1AaF1RlM_RB2s%3A%2Fapi%2Fecho&session_state=Mw52KcQijFB9Bq4rN-C4SF5Y&iss=https%3A%2F%2Fauth.hyeonworks.com%2Frealms%2Fkeycloak-patterns&code=f9a4835a-2af3-b886-bd04-10b5347ee8d2.Mw52KcQijFB9Bq4rN-C4SF5Y.80431dbc-af81-4673-9790-ad06d1570b2e:0
+[ 20374ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
diff --git a/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log b/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log
new file mode 100644
index 0000000..f9fd01b
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T06-25-42-707Z.log
@@ -0,0 +1 @@
+[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
diff --git a/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log b/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log
new file mode 100644
index 0000000..275afe6
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T06-26-41-840Z.log
@@ -0,0 +1 @@
+[ 423ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://app2.hyeonworks.com/favicon.ico:0
diff --git a/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log b/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log
new file mode 100644
index 0000000..58a3c36
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T07-14-56-538Z.log
@@ -0,0 +1,8 @@
+[ 1127ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 2377ms] [WARNING] is deprecated. Please include @ https://app2.hyeonworks.com/explore?schemaVersion=1&panes=%7B%22h4a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up+%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%22now-30m%22%2C%22to%22%3A%22now%22%7D%7D%7D&orgId=1:0
+[ 2472ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 3501ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 5119ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 8511ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 14956ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 28065ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
diff --git a/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log b/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log
new file mode 100644
index 0000000..8ac9793
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T07-15-33-300Z.log
@@ -0,0 +1,10 @@
+[ 1362ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 1874ms] [WARNING] is deprecated. Please include @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22up%7Bjob%3D%5C%22keycloak%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22up%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788502680000%22%2C%22to%22%3A%221788503520000%22%7D%7D%7D:0
+[ 2907ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 3998ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 6253ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 9426ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 12495ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 24486ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 31338ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 46196ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
diff --git a/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log b/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log
new file mode 100644
index 0000000..ba710d3
--- /dev/null
+++ b/.playwright-mcp/console-2026-09-04T07-16-34-745Z.log
@@ -0,0 +1,14 @@
+[ 1319ms] [WARNING] is deprecated. Please include @ https://app2.hyeonworks.com/explore?schemaVersion=1&orgId=1&panes=%7B%22a%22%3A%7B%22datasource%22%3A%22PBFA97CFB590B2093%22%2C%22queries%22%3A%5B%7B%22refId%22%3A%22A%22%2C%22expr%22%3A%22vendor_cluster_size%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22cluster_size%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%2C%7B%22refId%22%3A%22B%22%2C%22expr%22%3A%22vendor_statistics_approximate_entries_unique%7Bcache%3D%5C%22sessions%5C%22%7D%22%2C%22range%22%3Atrue%2C%22instant%22%3Afalse%2C%22editorMode%22%3A%22code%22%2C%22legendFormat%22%3A%22sessions%20%5Cuce90%5Cuc2dc%20%7B%7Bpod%7D%7D%22%2C%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22PBFA97CFB590B2093%22%7D%7D%5D%2C%22range%22%3A%7B%22from%22%3A%221788497040000%22%2C%22to%22%3A%221788499080000%22%7D%7D%7D:0
+[ 5941ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 11107ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 14234ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 17005ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 23049ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 30565ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 44135ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 54992ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 63653ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 70658ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 90768ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 105475ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
+[ 114995ms] [ERROR] WebSocket connection to 'wss://app2.hyeonworks.com/api/live/ws' failed: Error during WebSocket handshake: Unexpected response code: 400 @ https://app2.hyeonworks.com/public/build/1518.a3f1f690c084a37f01c7.js:362
diff --git a/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml b/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml
new file mode 100644
index 0000000..a7fde5e
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T06-25-15-925Z.yml
@@ -0,0 +1,16 @@
+- generic [ref=f55e3]:
+ - banner [ref=f55e4]:
+ - generic [ref=f55e5]: keycloak-patterns
+ - main [ref=f55e6]:
+ - heading "Sign in to your account" [level=1] [ref=f55e8]
+ - generic [ref=f55e12]:
+ - generic [ref=f55e13]:
+ - generic [ref=f55e14]: Username or email
+ - textbox "Username or email" [ref=f55e17]
+ - generic [ref=f55e18]:
+ - generic [ref=f55e19]: Password
+ - generic [ref=f55e21]:
+ - textbox "Password" [ref=f55e24]
+ - button "Show password" [ref=f55e26] [cursor=pointer]:
+ - generic [aria-hidden] [ref=f55e27]:
+ - button "Sign In" [ref=f55e30] [cursor=pointer]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml b/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml
new file mode 100644
index 0000000..aaf9d7c
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T06-25-26-485Z.yml
@@ -0,0 +1,16 @@
+- generic [ref=f55e3]:
+ - banner [ref=f55e4]:
+ - generic [ref=f55e5]: keycloak-patterns
+ - main [ref=f55e6]:
+ - heading "Sign in to your account" [level=1] [ref=f55e8]
+ - generic [ref=f55e12]:
+ - generic [ref=f55e13]:
+ - generic [ref=f55e14]: Username or email
+ - textbox "Username or email" [ref=f55e17]: labuser
+ - generic [ref=f55e18]:
+ - generic [ref=f55e19]: Password
+ - generic [ref=f55e21]:
+ - textbox "Password" [ref=f55e24]: labpass
+ - button "Show password" [ref=f55e26] [cursor=pointer]:
+ - generic [aria-hidden] [ref=f55e27]:
+ - button "Sign In" [ref=f55e30] [cursor=pointer]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml b/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml
new file mode 100644
index 0000000..7b140eb
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T06-25-35-208Z.yml
@@ -0,0 +1,17 @@
+- generic [ref=f56e1]:
+ - generic [ref=f56e3]:
+ - generic [ref=f56e4]: "403"
+ - heading "Forbidden" [level=1] [ref=f56e6]
+ - generic [ref=f56e8]:
+ - paragraph [ref=f56e9]: More Info
+ - generic [ref=f56e10] [cursor=pointer]:
+ - separator [ref=f56e12]
+ - generic [ref=f56e13]:
+ - button "Go back" [ref=f56e16] [cursor=pointer]
+ - button "Sign in" [ref=f56e19] [cursor=pointer]
+ - contentinfo [ref=f56e20]:
+ - paragraph [ref=f56e22]:
+ - text: Secured with
+ - link "OAuth2 Proxy" [ref=f56e23] [cursor=pointer]:
+ - /url: https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy
+ - text: version v7.7.1
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml b/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml
new file mode 100644
index 0000000..8b3d9cd
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T06-25-42-934Z.yml
@@ -0,0 +1 @@
+- generic [ref=f57e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMDNZMkZrTUdZM01tRmlZekkwWldFell6a3lOREJpTW1KaE5UZGpOVEJoWWcuZVVmckp5VHRqY1VsXzdiV1hiX3hwdw==|1788503135|yDSo7VdgRSlvoVfj9raHPClKTlQiWDg7FauLfoUayw4=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.0.53\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml b/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml
new file mode 100644
index 0000000..bfa5b33
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T06-26-42-277Z.yml
@@ -0,0 +1 @@
+- generic [ref=f58e1]: "{ \"headers\" : { \"host\" : [ \"app2.hyeonworks.com\" ], \"user-agent\" : [ \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\" ], \"accept\" : [ \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\" ], \"accept-encoding\" : [ \"gzip, deflate, br, zstd\" ], \"accept-language\" : [ \"en-US,en;q=0.9\" ], \"cookie\" : [ \"grafana_session=60c3e7ae41ffc00665f4a2c377def399; grafana_session_expiry=1788497124; _oauth2_proxy=djIuWDI5aGRYUm9NbDl3Y205NGVTMHpabUV5TVdKaVpEa3lOemRrTlRFMU9USTBaV00wWWpGaE16bGhNak0zT1EuN2tTa3dnWUdISDkwMGFSSTVOSUFFUQ==|1788503202|snWKU5IRfRLoD9-bXEodGjEgfHeAw8PQaoHnecpFH90=\" ], \"priority\" : [ \"u=0, i\" ], \"sec-ch-ua\" : [ \"\\\"Chromium\\\";v=\\\"152\\\", \\\"Not?A_Brand\\\";v=\\\"24\\\", \\\"Google Chrome\\\";v=\\\"152\\\"\" ], \"sec-ch-ua-mobile\" : [ \"?0\" ], \"sec-ch-ua-platform\" : [ \"\\\"Linux\\\"\" ], \"sec-fetch-dest\" : [ \"document\" ], \"sec-fetch-mode\" : [ \"navigate\" ], \"sec-fetch-site\" : [ \"none\" ], \"sec-fetch-user\" : [ \"?1\" ], \"upgrade-insecure-requests\" : [ \"1\" ], \"x-forwarded-email\" : [ \"changed-labuser@example.com\" ], \"x-forwarded-host\" : [ \"app2.hyeonworks.com\" ], \"x-forwarded-port\" : [ \"443\" ], \"x-forwarded-preferred-username\" : [ \"labuser\" ], \"x-forwarded-proto\" : [ \"https\" ], \"x-forwarded-server\" : [ \"traefik-5d6fcf895-wpfhr\" ], \"x-forwarded-user\" : [ \"27df5ea9-8703-4ec5-badd-d972c583e1ff\" ], \"x-real-ip\" : [ \"100.123.124.30\" ] }, \"remoteAddr\" : \"100.123.124.30\", \"localAddr\" : \"10.42.1.132\", \"scheme\" : \"https\", \"secure\" : true, \"serverName\" : \"app2.hyeonworks.com\", \"serverPort\" : 443, \"requestUrl\" : \"https://app2.hyeonworks.com/api/echo\" }"
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml b/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml
new file mode 100644
index 0000000..3165cde
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T07-14-59-023Z.yml
@@ -0,0 +1,175 @@
+- generic [ref=f59e1]:
+ - generic [ref=f59e4]:
+ - link "Skip to main content" [ref=f59e5] [cursor=pointer]:
+ - /url: "#pageContent"
+ - banner [ref=f59e7]:
+ - generic [ref=f59e8]:
+ - link [ref=f59e10] [cursor=pointer]:
+ - /url: /
+ - img "Grafana" [ref=f59e11]
+ - generic [ref=f59e14]:
+ - button "Search or jump to..." [ref=f59e18] [cursor=pointer]
+ - generic [ref=f59e19]: ctrl+k
+ - generic [ref=f59e23]:
+ - button "New" [ref=f59e24] [cursor=pointer]
+ - button "Help" [ref=f59e30] [cursor=pointer]
+ - button "News" [ref=f59e33] [cursor=pointer]
+ - button "Profile" [ref=f59e36] [cursor=pointer]:
+ - img "User avatar" [ref=f59e37]
+ - generic [ref=f59e38]:
+ - button "Open menu" [ref=f59e40] [cursor=pointer]
+ - navigation "Breadcrumbs" [ref=f59e43]:
+ - list [ref=f59e44]:
+ - listitem [ref=f59e45]:
+ - link "Home" [ref=f59e46] [cursor=pointer]:
+ - /url: /
+ - listitem [ref=f59e50]:
+ - link "Explore" [ref=f59e51] [cursor=pointer]:
+ - /url: /explore
+ - listitem [ref=f59e55]:
+ - generic "Prometheus" [ref=f59e56]
+ - generic [ref=f59e57]:
+ - generic [ref=f59e60]:
+ - button "Copy shortened URL" [ref=f59e61] [cursor=pointer]
+ - button "Open copy link options" [ref=f59e64] [cursor=pointer]
+ - button "Toggle top search bar" [ref=f59e68] [cursor=pointer]
+ - main [ref=f59e74]:
+ - generic [ref=f59e76]:
+ - heading "Explore" [level=1] [ref=f59e77]
+ - generic [ref=f59e82]:
+ - navigation "Explore toolbar" [ref=f59e84]:
+ - navigation "Search links" [ref=f59e86]:
+ - generic [ref=f59e87]:
+ - button "Content outline" [expanded] [ref=f59e89] [cursor=pointer]:
+ - generic [ref=f59e92]: Outline
+ - generic [ref=f59e97] [cursor=pointer]:
+ - img "Prometheus logo" [ref=f59e99]
+ - textbox "Select a data source" [ref=f59e100]:
+ - /placeholder: Prometheus
+ - generic [ref=f59e104]:
+ - button "Split the pane" [ref=f59e106] [cursor=pointer]:
+ - generic [ref=f59e109]: Split
+ - button "Add" [ref=f59e111] [cursor=pointer]
+ - generic [ref=f59e116]:
+ - 'button "Time range selected: Last 30 minutes" [ref=f59e117] [cursor=pointer]'
+ - button "Zoom out time range" [ref=f59e122] [cursor=pointer]
+ - generic [ref=f59e126]:
+ - button "Cancel" [ref=f59e127] [cursor=pointer]
+ - button "Auto refresh turned off. Choose refresh time interval" [ref=f59e129] [cursor=pointer]
+ - generic [ref=f59e133]:
+ - generic [ref=f59e137]:
+ - button "Collapse outline" [expanded] [ref=f59e139] [cursor=pointer]:
+ - img "arrow-from-right" [ref=f59e140]
+ - generic [ref=f59e142]:
+ - button "Content outline item collapse button" [ref=f59e143] [cursor=pointer]:
+ - img "angle-right" [ref=f59e144]
+ - button "Queries" [ref=f59e146] [cursor=pointer]:
+ - img "arrow" [ref=f59e147]
+ - generic [ref=f59e154]:
+ - generic [ref=f59e156]:
+ - generic [ref=f59e157]:
+ - generic "Query editor row" [ref=f59e159]:
+ - generic [ref=f59e160]:
+ - generic [ref=f59e162]:
+ - generic [ref=f59e163]:
+ - button "Collapse query row" [expanded] [ref=f59e164] [cursor=pointer]
+ - generic [ref=f59e167]:
+ - button "Query editor row title A" [ref=f59e168] [cursor=pointer]:
+ - generic [ref=f59e169]: A
+ - emphasis [ref=f59e170]: (Prometheus)
+ - generic [ref=f59e171]:
+ - button "Show data source help" [ref=f59e173] [cursor=pointer]
+ - button "Duplicate query" [ref=f59e177] [cursor=pointer]
+ - button "Hide response" [ref=f59e181] [cursor=pointer]
+ - button "Remove query" [ref=f59e185] [cursor=pointer]
+ - button "Drag and drop to reorder" [ref=f59e188]:
+ - img "Drag and drop to reorder" [ref=f59e189]
+ - generic [ref=f59e192]:
+ - generic [ref=f59e193]:
+ - button "Kick start your query" [ref=f59e194] [cursor=pointer]
+ - generic [ref=f59e197]:
+ - generic [ref=f59e198] [cursor=pointer]: Explain
+ - generic [ref=f59e199]:
+ - checkbox "Explain Toggle switch" [ref=f59e200]
+ - generic "Toggle switch" [ref=f59e201] [cursor=pointer]
+ - radiogroup [ref=f59e206]:
+ - generic [ref=f59e207]:
+ - radio "Builder" [ref=f59e208] [cursor=pointer]
+ - generic [ref=f59e209] [cursor=pointer]: Builder
+ - generic [ref=f59e210]:
+ - radio "Code" [checked] [ref=f59e211] [cursor=pointer]
+ - generic [ref=f59e212] [cursor=pointer]: Code
+ - generic [ref=f59e214]:
+ - generic [ref=f59e216]:
+ - button "Loading metrics..." [disabled] [ref=f59e217] [cursor=pointer]
+ - code [ref=f59e224]:
+ - generic [ref=f59e225]:
+ - generic [ref=f59e230]: vendor_cluster_size
+ - textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e235]: vendor_cluster_size
+ - 'button "Options Legend: cluster_size {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e241] [cursor=pointer]':
+ - generic [ref=f59e245]:
+ - heading "Options" [level=6] [ref=f59e246]
+ - generic [ref=f59e247]:
+ - generic [ref=f59e248]: "Legend: cluster_size {{pod}}"
+ - generic [ref=f59e249]: "Format: Time series"
+ - generic [ref=f59e250]: "Step: auto"
+ - generic [ref=f59e251]: "Type: Range"
+ - generic [ref=f59e252]: "Exemplars: false"
+ - generic "Query editor row" [ref=f59e254]:
+ - generic [ref=f59e255]:
+ - generic [ref=f59e257]:
+ - generic [ref=f59e258]:
+ - button "Collapse query row" [expanded] [ref=f59e259] [cursor=pointer]
+ - generic [ref=f59e262]:
+ - button "Query editor row title B" [ref=f59e263] [cursor=pointer]:
+ - generic [ref=f59e264]: B
+ - emphasis [ref=f59e265]: (Prometheus)
+ - generic [ref=f59e266]:
+ - button "Show data source help" [ref=f59e268] [cursor=pointer]
+ - button "Duplicate query" [ref=f59e272] [cursor=pointer]
+ - button "Hide response" [ref=f59e276] [cursor=pointer]
+ - button "Remove query" [ref=f59e280] [cursor=pointer]
+ - button "Drag and drop to reorder" [ref=f59e283]:
+ - img "Drag and drop to reorder" [ref=f59e284]
+ - generic [ref=f59e287]:
+ - generic [ref=f59e288]:
+ - button "Kick start your query" [ref=f59e289] [cursor=pointer]
+ - generic [ref=f59e292]:
+ - generic [ref=f59e293] [cursor=pointer]: Explain
+ - generic [ref=f59e294]:
+ - checkbox "Explain Toggle switch" [ref=f59e295]
+ - generic "Toggle switch" [ref=f59e296] [cursor=pointer]
+ - radiogroup [ref=f59e301]:
+ - generic [ref=f59e302]:
+ - radio "Builder" [ref=f59e303] [cursor=pointer]
+ - generic [ref=f59e304] [cursor=pointer]: Builder
+ - generic [ref=f59e305]:
+ - radio "Code" [checked] [ref=f59e306] [cursor=pointer]
+ - generic [ref=f59e307] [cursor=pointer]: Code
+ - generic [ref=f59e309]:
+ - generic [ref=f59e311]:
+ - button "Loading metrics..." [disabled] [ref=f59e312] [cursor=pointer]
+ - code [ref=f59e319]:
+ - generic [ref=f59e320]:
+ - generic [ref=f59e325]: "up{job=\"keycloak\"}"
+ - textbox "Editor content;Press Alt+F1 for Accessibility Options." [ref=f59e330]: "up{job=\"keycloak\"}"
+ - 'button "Options Legend: up {{pod}} Format: Time series Step: auto Type: Range Exemplars: false" [ref=f59e336] [cursor=pointer]':
+ - generic [ref=f59e340]:
+ - heading "Options" [level=6] [ref=f59e341]
+ - generic [ref=f59e342]:
+ - generic [ref=f59e343]: "Legend: up {{pod}}"
+ - generic [ref=f59e344]: "Format: Time series"
+ - generic [ref=f59e345]: "Step: auto"
+ - generic [ref=f59e346]: "Type: Range"
+ - generic [ref=f59e347]: "Exemplars: false"
+ - generic [ref=f59e348]:
+ - button "Add query" [ref=f59e349] [cursor=pointer]
+ - button "Query history" [ref=f59e353] [cursor=pointer]
+ - button "Query inspector" [ref=f59e357] [cursor=pointer]
+ - generic:
+ - main
+ - generic [ref=f59e364]:
+ - alert
+ - alert
+ - complementary
+ - complementary
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml b/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml
new file mode 100644
index 0000000..8bbdcf1
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T07-15-35-378Z.yml
@@ -0,0 +1,56 @@
+- generic [ref=f62e4]:
+ - link "Skip to main content" [ref=f62e5] [cursor=pointer]:
+ - /url: "#pageContent"
+ - banner [ref=f62e7]:
+ - generic [ref=f62e8]:
+ - link [ref=f62e10] [cursor=pointer]:
+ - /url: /
+ - img "Grafana" [ref=f62e11]
+ - generic [ref=f62e14]:
+ - button "Search or jump to..." [ref=f62e18] [cursor=pointer]
+ - generic [ref=f62e19]: ctrl+k
+ - generic [ref=f62e23]:
+ - button "New" [ref=f62e24] [cursor=pointer]
+ - button "Help" [ref=f62e30] [cursor=pointer]
+ - button "News" [ref=f62e33] [cursor=pointer]
+ - button "Profile" [ref=f62e36] [cursor=pointer]:
+ - img "User avatar" [ref=f62e37]
+ - generic [ref=f62e38]:
+ - button "Open menu" [ref=f62e40] [cursor=pointer]
+ - navigation "Breadcrumbs" [ref=f62e43]:
+ - list [ref=f62e44]:
+ - listitem [ref=f62e45]:
+ - link "Home" [ref=f62e46] [cursor=pointer]:
+ - /url: /
+ - listitem [ref=f62e50]:
+ - link "Explore" [ref=f62e51] [cursor=pointer]:
+ - /url: /explore
+ - listitem [ref=f62e55]:
+ - generic "Prometheus" [ref=f62e56]
+ - generic [ref=f62e57]:
+ - button "Show more items" [ref=f62e60] [cursor=pointer]
+ - button "Toggle top search bar" [ref=f62e64] [cursor=pointer]
+ - main [ref=f62e70]:
+ - generic [ref=f62e72]:
+ - heading "Explore" [level=1] [ref=f62e73]
+ - generic [ref=f62e78]:
+ - navigation "Explore toolbar" [ref=f62e80]:
+ - navigation "Search links" [ref=f62e82]:
+ - generic [ref=f62e83]:
+ - button "Content outline" [expanded] [ref=f62e85] [cursor=pointer]:
+ - generic [ref=f62e88]: Outline
+ - generic [ref=f62e93] [cursor=pointer]:
+ - img "Prometheus logo" [ref=f62e95]
+ - textbox "Select a data source" [ref=f62e96]:
+ - /placeholder: Prometheus
+ - button "Show more items" [ref=f62e102] [cursor=pointer]
+ - generic [ref=f62e106]:
+ - button "Collapse outline" [expanded] [ref=f62e112] [cursor=pointer]:
+ - img "arrow-from-right" [ref=f62e113]
+ - generic [ref=f62e119]:
+ - generic [ref=f62e122]:
+ - button "Add query" [ref=f62e123] [cursor=pointer]
+ - button "Query history" [ref=f62e127] [cursor=pointer]
+ - button "Query inspector" [ref=f62e131] [cursor=pointer]
+ - generic:
+ - main
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml b/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml
new file mode 100644
index 0000000..8c6fd4c
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-04T07-16-36-528Z.yml
@@ -0,0 +1,29 @@
+- generic [ref=f65e4]:
+ - link "Skip to main content" [ref=f65e5] [cursor=pointer]:
+ - /url: "#pageContent"
+ - banner [ref=f65e7]:
+ - generic [ref=f65e8]:
+ - link [ref=f65e10] [cursor=pointer]:
+ - /url: /
+ - img "Grafana" [ref=f65e11]
+ - generic [ref=f65e14]:
+ - button "Search or jump to..." [ref=f65e18] [cursor=pointer]
+ - generic [ref=f65e19]: ctrl+k
+ - generic [ref=f65e23]:
+ - button "New" [ref=f65e24] [cursor=pointer]
+ - button "Help" [ref=f65e30] [cursor=pointer]
+ - button "News" [ref=f65e33] [cursor=pointer]
+ - button "Profile" [ref=f65e36] [cursor=pointer]:
+ - img "User avatar" [ref=f65e37]
+ - generic [ref=f65e38]:
+ - button "Open menu" [ref=f65e40] [cursor=pointer]
+ - navigation "Breadcrumbs" [ref=f65e43]:
+ - list [ref=f65e44]:
+ - listitem [ref=f65e45]:
+ - link "Home" [ref=f65e46] [cursor=pointer]:
+ - /url: /
+ - listitem [ref=f65e50]:
+ - generic "Explore" [ref=f65e51]
+ - button "Toggle top search bar" [ref=f65e53] [cursor=pointer]
+ - main [ref=f65e59]:
+ - heading "Explore" [level=1] [ref=f65e62]
\ No newline at end of file
diff --git a/docs/diagrams/_style.md b/docs/diagrams/_style.md
new file mode 100644
index 0000000..43dbdb3
--- /dev/null
+++ b/docs/diagrams/_style.md
@@ -0,0 +1,12 @@
+# 다이어그램 규약
+
+| 표현 | 뜻 |
+|---|---|
+| 실선 상자 | 살아 있는 구성 요소 |
+| 붉은 점선 상자 | 이 실험에서 죽이거나 막은 것 |
+| ✂ 붉은 X | 주입 지점 |
+| 실선 화살표 | 정상 경로 |
+| 붉은 점선 화살표 | 실험에서 깨진 경로 |
+| 회색 글씨 | 측정값 |
+
+SVG 는 GitHub 에서 그대로 렌더링되며 외부 폰트를 쓰지 않는다.
diff --git a/docs/diagrams/a2-database-loss.svg b/docs/diagrams/a2-database-loss.svg
new file mode 100644
index 0000000..f8dc35a
--- /dev/null
+++ b/docs/diagrams/a2-database-loss.svg
@@ -0,0 +1,26 @@
+
diff --git a/docs/diagrams/a7-volatile-inversion.svg b/docs/diagrams/a7-volatile-inversion.svg
new file mode 100644
index 0000000..d93e189
--- /dev/null
+++ b/docs/diagrams/a7-volatile-inversion.svg
@@ -0,0 +1,36 @@
+
diff --git a/docs/diagrams/b1-store-split.svg b/docs/diagrams/b1-store-split.svg
new file mode 100644
index 0000000..24dcfe5
--- /dev/null
+++ b/docs/diagrams/b1-store-split.svg
@@ -0,0 +1,33 @@
+
diff --git a/docs/diagrams/b3-refresh-contention.svg b/docs/diagrams/b3-refresh-contention.svg
new file mode 100644
index 0000000..5f614a4
--- /dev/null
+++ b/docs/diagrams/b3-refresh-contention.svg
@@ -0,0 +1,29 @@
+
diff --git a/docs/diagrams/b4-header-forgery.svg b/docs/diagrams/b4-header-forgery.svg
new file mode 100644
index 0000000..e6753f1
--- /dev/null
+++ b/docs/diagrams/b4-header-forgery.svg
@@ -0,0 +1,37 @@
+
diff --git a/docs/diagrams/b5-redis-loss.svg b/docs/diagrams/b5-redis-loss.svg
new file mode 100644
index 0000000..cfc0eee
--- /dev/null
+++ b/docs/diagrams/b5-redis-loss.svg
@@ -0,0 +1,35 @@
+
diff --git a/docs/diagrams/b6-key-rotation.svg b/docs/diagrams/b6-key-rotation.svg
new file mode 100644
index 0000000..e021a70
--- /dev/null
+++ b/docs/diagrams/b6-key-rotation.svg
@@ -0,0 +1,34 @@
+
diff --git a/docs/diagrams/c1-sso-structure.svg b/docs/diagrams/c1-sso-structure.svg
new file mode 100644
index 0000000..6f87ddc
--- /dev/null
+++ b/docs/diagrams/c1-sso-structure.svg
@@ -0,0 +1,31 @@
+
diff --git a/docs/diagrams/d1-backup-restore.svg b/docs/diagrams/d1-backup-restore.svg
new file mode 100644
index 0000000..a9618ae
--- /dev/null
+++ b/docs/diagrams/d1-backup-restore.svg
@@ -0,0 +1,30 @@
+
diff --git a/docs/diagrams/d3-secret-exposure.svg b/docs/diagrams/d3-secret-exposure.svg
new file mode 100644
index 0000000..5e6ee4d
--- /dev/null
+++ b/docs/diagrams/d3-secret-exposure.svg
@@ -0,0 +1,31 @@
+
diff --git a/docs/diagrams/d4-cert-chain.svg b/docs/diagrams/d4-cert-chain.svg
new file mode 100644
index 0000000..16be38d
--- /dev/null
+++ b/docs/diagrams/d4-cert-chain.svg
@@ -0,0 +1,30 @@
+
diff --git a/docs/diagrams/lab-topology.svg b/docs/diagrams/lab-topology.svg
new file mode 100644
index 0000000..eb39cd3
--- /dev/null
+++ b/docs/diagrams/lab-topology.svg
@@ -0,0 +1,57 @@
+
diff --git a/docs/evidence/d2-version-upgrade/d2-upgrade-window.png b/docs/evidence/d2-version-upgrade/d2-upgrade-window.png
new file mode 100644
index 0000000..4b4e614
Binary files /dev/null and b/docs/evidence/d2-version-upgrade/d2-upgrade-window.png differ
diff --git a/docs/evidence/followup/01-d2-forward-upgrade.txt b/docs/evidence/followup/01-d2-forward-upgrade.txt
new file mode 100644
index 0000000..879a804
--- /dev/null
+++ b/docs/evidence/followup/01-d2-forward-upgrade.txt
@@ -0,0 +1,32 @@
+=== D-1 절차대로 먼저 백업 ===
+ 백업: 395375 bytes
+ 마이그레이션 전: 210
+ 세션 전: 3
+
+=== ★ 정방향 업그레이드 + 1초 간격 가용성 측정 ===
+ 시작: 15:22:59
+partitioned roll out complete: 2 new pods have been updated...
+ 완료: 15:24:26
+
+=== 업그레이드 중 외부 응답 시계열 ===
+ 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200 200 200 200
+ 200 응답: 87 회
+ 비200 : 0
+0 회
+=== 업그레이드 후 ===
+quay.io/keycloak/keycloak:26.7.3
+ Keycloak 26.7.3
+ 마이그레이션 후: 210 (전: 210)
+ 세션 후: 3 (전: 3)
+
+=== 스키마 마이그레이션이 실제로 있었는가 ===
+ (없으면 26.7.0→26.7.3 에 스키마 변경이 없다는 뜻)
+
+=== 파드 상태와 클러스터 ===
+ keycloak-0 1/1 Running restarts=0
+ keycloak-1 1/1 Running restarts=0
+ cluster: [keycloak-1-11418(v=16.0.14)|47] (2) [keycloak-1-11418(v=16.0.14), keycloak-0-58996(v=16.0.14)]
diff --git a/docs/evidence/followup/02-d2-rollback-same-schema.txt b/docs/evidence/followup/02-d2-rollback-same-schema.txt
new file mode 100644
index 0000000..b4d6af6
--- /dev/null
+++ b/docs/evidence/followup/02-d2-rollback-same-schema.txt
@@ -0,0 +1,20 @@
+=== ★ 가설: 스키마 변경이 없으면 롤백이 된다 (26.7.3 → 26.7.0) ===
+ 시작: 15:25:08
+partitioned roll out complete: 2 new pods have been updated...
+ 완료: 15:25:53
+
+ 200 응답: 43 회 / 비200: 1
+
+ keycloak-0 1/1 Running restarts=0
+ keycloak-1 1/1 Running restarts=0
+ Keycloak 26.7.0
+ 마이그레이션: 210
+ 세션: 3
+=== 롤백 중 응답 시계열 (비200 위치) ===
+ 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200 000 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
+ 200 200 200 200
+ 비200 값: 000
+
+=== 대조: 정방향 업그레이드 때는 ===
+ 200: 87 / 비200: 0
diff --git a/docs/evidence/followup/03-b4-role-propagation.txt b/docs/evidence/followup/03-b4-role-propagation.txt
new file mode 100644
index 0000000..d0dbbc5
--- /dev/null
+++ b/docs/evidence/followup/03-b4-role-propagation.txt
@@ -0,0 +1,9 @@
+=== ★ Keycloak 에서 email 을 바꾼다 ===
+ 변경 시각: 15:27:52
+ IdP 의 값: [ {
+ IdP 의 값: "email" : "changed-labuser@example.com"
+ IdP 의 값: } ]
+
+=== IdP 쪽 세션과 oauth2-proxy 세션 ===
+ Redis 세션: 1 개
+ (세션은 로그인 시점의 클레임을 담고 있다 — 이제 요청을 반복해 본다)
diff --git a/docs/evidence/followup/04-observability-gap.txt b/docs/evidence/followup/04-observability-gap.txt
new file mode 100644
index 0000000..8f080c9
--- /dev/null
+++ b/docs/evidence/followup/04-observability-gap.txt
@@ -0,0 +1,17 @@
+=== Prometheus 가 실제로 긁는 대상 (2026-09-04 18:10 KST) ===
+ keycloak 2개
+ kubelet 2개
+ node-exporter 2개
+ prometheus 1개
+
+=== B층 구성 요소의 지표가 있는가 ===
+ redis_up 시계열 0개
+ redis_connected_clients 시계열 0개
+ redis_memory_used_bytes 시계열 0개
+ pg_up 시계열 0개
+ pg_stat_database_numbackends 시계열 0개
+
+ → B-1·B-2·B-3·B-5 는 Grafana 증거를 만들 수 없다.
+ 스크린샷을 안 찍은 것이 아니라 긁는 대상에 없다.
+ 보완하려면 redis_exporter · postgres_exporter · BFF 의 /actuator/prometheus 를
+ scrape 대상에 추가해야 한다.
diff --git a/docs/evidence/followup/README.md b/docs/evidence/followup/README.md
new file mode 100644
index 0000000..820a980
--- /dev/null
+++ b/docs/evidence/followup/README.md
@@ -0,0 +1,17 @@
+# 후속 — 미측정으로 남겼던 항목을 채운 기록
+
+2026-09-04 17:35–18:20 KST
+해설: [`docs/experiment-followup-untested-items.md`](../../experiment-followup-untested-items.md)
+
+| 파일 | 무엇을 보여주는가 |
+|---|---|
+| `01-d2-forward-upgrade.txt` | **D-2 정방향** 26.7.0 → 26.7.3. 백업 396KB · **87회 요청 전부 200(무중단)** · 마이그레이션 210 → 210(스키마 변경 없음) · 세션 3 유지 · Infinispan 16.0.12 → 16.0.14 |
+| `02-d2-rollback-same-schema.txt` | **스키마가 안 바뀌면 롤백이 된다** — 26.7.3 → 26.7.0 성공. 다만 전환 순간 `000` 1회(3초 타임아웃) |
+| `03-b4-role-propagation.txt` | **B-4 ③** IdP 에서 값을 바꿔도 **12회 요청·6초 동안 옛 값**. 세션 삭제 후 재인증에서야 새 값 |
+| `04-observability-gap.txt` | **B층에 관측이 없다** — Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다. Redis·BFF·PostgreSQL 지표가 0개 |
+
+## 핵심 세 줄
+
+1. **"롤백은 안 된다" 는 조건부였다.** 스키마가 바뀌었으면 안 되고, 안 바뀌었으면 된다 — D-2 의 결론을 정밀화한다.
+2. **role 변경은 요청 횟수와 무관하게 반영되지 않는다.** `--cookie-refresh` 가 없으면 쿠키 만료나 재인증까지 옛 값이 간다.
+3. **B층 실험에 Grafana 증거가 없는 이유가 확인됐다** — 관측 대상에 애초에 없다. 스크린샷이 없는 것이 아니라 지표가 없다.
diff --git a/docs/experiment-a2-database-loss.md b/docs/experiment-a2-database-loss.md
index 37f01d0..eac488b 100644
--- a/docs/experiment-a2-database-loss.md
+++ b/docs/experiment-a2-database-loss.md
@@ -9,6 +9,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| 예측 | 결과 |
diff --git a/docs/experiment-a7-volatile-comparison.md b/docs/experiment-a7-volatile-comparison.md
index 091165e..73daa2c 100644
--- a/docs/experiment-a7-volatile-comparison.md
+++ b/docs/experiment-a7-volatile-comparison.md
@@ -9,6 +9,14 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+
+---
+
## 0. 결론부터 — 비교표
| 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** |
diff --git a/docs/experiment-b1-redis-session-store.md b/docs/experiment-b1-redis-session-store.md
index 2040926..4508167 100644
--- a/docs/experiment-b1-redis-session-store.md
+++ b/docs/experiment-b1-redis-session-store.md
@@ -10,6 +10,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| | before | after | |
diff --git a/docs/experiment-b3-refresh-token-contention.md b/docs/experiment-b3-refresh-token-contention.md
index 15f6f79..27c87c2 100644
--- a/docs/experiment-b3-refresh-token-contention.md
+++ b/docs/experiment-b3-refresh-token-contention.md
@@ -12,6 +12,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
**"하나는 성공하고 하나는 실패한다"가 아니다. 세션이 파괴된다.**
diff --git a/docs/experiment-b4-edge-authorization-scope.md b/docs/experiment-b4-edge-authorization-scope.md
index 1c50e14..2753815 100644
--- a/docs/experiment-b4-edge-authorization-scope.md
+++ b/docs/experiment-b4-edge-authorization-scope.md
@@ -10,6 +10,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| Q4 의 미지수 | 측정 결과 |
diff --git a/docs/experiment-b5-redis-loss-persistence.md b/docs/experiment-b5-redis-loss-persistence.md
index 17f818b..7e40350 100644
--- a/docs/experiment-b5-redis-loss-persistence.md
+++ b/docs/experiment-b5-redis-loss-persistence.md
@@ -8,6 +8,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| | 결과 |
diff --git a/docs/experiment-b6-key-rotation.md b/docs/experiment-b6-key-rotation.md
index 5698aad..8f8ff28 100644
--- a/docs/experiment-b6-key-rotation.md
+++ b/docs/experiment-b6-key-rotation.md
@@ -12,6 +12,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
**질문이 두 갈래로 나뉜다.**
diff --git a/docs/experiment-c1-multi-app-sso.md b/docs/experiment-c1-multi-app-sso.md
index d06976e..3e61c02 100644
--- a/docs/experiment-c1-multi-app-sso.md
+++ b/docs/experiment-c1-multi-app-sso.md
@@ -11,6 +11,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| 물음 | 답 |
diff --git a/docs/experiment-d1-backup-restore.md b/docs/experiment-d1-backup-restore.md
index 8fc7f86..51b50a2 100644
--- a/docs/experiment-d1-backup-restore.md
+++ b/docs/experiment-d1-backup-restore.md
@@ -8,6 +8,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| 측정 | 값 |
diff --git a/docs/experiment-d3-secret-management.md b/docs/experiment-d3-secret-management.md
index eb37e3a..b516523 100644
--- a/docs/experiment-d3-secret-management.md
+++ b/docs/experiment-d3-secret-management.md
@@ -6,6 +6,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| 경로 | 감춰지는가 |
diff --git a/docs/experiment-d4-certificate-renewal.md b/docs/experiment-d4-certificate-renewal.md
index 65aecbc..5332d59 100644
--- a/docs/experiment-d4-certificate-renewal.md
+++ b/docs/experiment-d4-certificate-renewal.md
@@ -6,6 +6,15 @@
---
+## 구조
+
+
+
+> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
+> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
+
+---
+
## 0. 결론부터
| 확인 | 결과 |
diff --git a/docs/experiment-followup-untested-items.md b/docs/experiment-followup-untested-items.md
new file mode 100644
index 0000000..fb2ad15
--- /dev/null
+++ b/docs/experiment-followup-untested-items.md
@@ -0,0 +1,249 @@
+# 후속 — 미측정으로 남겼던 항목을 채운다
+
+브랜치 `feature/keycloak-followup-untested-items` ·
+증거 [`docs/evidence/followup/`](evidence/followup/) ·
+2026-09-04 17:35–18:20 KST
+
+23개 실험을 마치며 **세 항목을 "못 했다" 로 남겼다.** 그중 둘을 채우고,
+셋째(D-4 강제 갱신)는 권한이 필요해 별도로 진행한다.
+
+---
+
+## 0. 결론부터
+
+| 항목 | 결과 |
+|---|---|
+| **D-2 정방향 업그레이드** | **무중단.** 87회 요청 전부 200 |
+| **그리고 D-2 의 결론이 정밀해졌다** | **"롤백 불가" 는 조건부다** — 스키마가 바뀌었을 때만 |
+| **B-4 ③ role 변경 반영 시점** | **요청 횟수와 무관하다.** 세션이 새로 만들어져야 한다 |
+| **B층에 Grafana 증거가 없는 이유** | **관측 대상에 없다.** 안 찍은 것이 아니다 |
+
+---
+
+## 1. D-2 정방향 업그레이드 — 26.7.0 → 26.7.3
+
+### 개념 — 왜 이 방향을 못 했었나
+
+D-2 를 처음 할 때 26.7.0 보다 새 이미지를 몰라 **역방향(26.0)만 시험**했다.
+태그 목록을 조회하니 26.7.1 / 26.7.2 / **26.7.3** 이 있었다.
+
+```bash
+curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
+```
+
+### 절차 — D-1 의 교훈대로 백업이 먼저다
+
+```bash
+kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
+ --clean --if-exists > /tmp/pre-2673.sql # 396333 bytes
+
+# 1초 간격으로 외부 진입점을 찍으면서 태그를 바꾼다
+( for i in $(seq 1 150); do
+ printf "%s " "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 https://auth.hyeonworks.com/realms/master)"
+ sleep 1
+ done > /tmp/avail.txt ) &
+kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.3
+kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
+```
+
+### 결과 — 무중단
+
+```
+200 200 200 ... (87회)
+ 200 응답: 87 회
+ 비200 : 0 회
+소요: 15:22:59 → 15:24:26 (87초)
+```
+
+
+
+**파드가 하나씩 교체되며 `cluster_size` 가 2 → 1 → 2 를 두 번 반복한다.**
+각 파드의 `up` 시계열이 끝나고 새 시계열이 시작되는 것이 함께 보인다.
+
+```
+ 마이그레이션: 210 → 210 ← 스키마 변경 없음
+ 세션: 3 → 3 ← 유지
+ Infinispan: 16.0.12 → 16.0.14
+ restarts=0
+```
+
+---
+
+## 2. ★ 그래서 D-2 의 결론을 정밀화한다
+
+**"스키마 변경이 없었다면 롤백이 될 것" 이라는 가설이 생겼고, 시험했다.**
+
+```bash
+kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
+```
+
+```
+ 200 응답: 43 회 / 비200: 1
+ Keycloak 26.7.0
+ 마이그레이션: 210 · 세션: 3
+ restarts=0
+```
+
+**롤백이 성공했다.**
+
+| 버전 차 | `databasechangelog` | 롤백 |
+|---|---|---|
+| 26.7.0 → **26.0** | 체크섬 불일치 | **불가** (`ValidationFailedException`) |
+| 26.7.0 ↔ **26.7.3** | **210 → 210, 변화 없음** | **가능** |
+
+> **처음 D-2 에서 "롤백은 안 된다" 고 쓴 것은 과했다.**
+> 정확히는 **"스키마가 바뀌었으면 안 된다"** 이고,
+> **패치 릴리스처럼 스키마가 그대로면 태그를 되돌리는 것으로 충분하다.**
+>
+> 판단 기준은 버전 번호가 아니라 **`databasechangelog` 의 행 수가 바뀌었는가**다.
+
+```bash
+# 업그레이드 전후로 이것만 비교하면 롤백 가능 여부를 안다
+kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
+ "select count(*) from databasechangelog"
+```
+
+### 전환 순간의 `000` 1회
+
+```
+200 ×24 000 200 ×19
+```
+
+**서버 오류가 아니라 `--max-time 3` 타임아웃**이다. 파드 전환 순간 요청 하나가
+3초를 넘겼다. 정방향에서는 0회였다.
+
+> **"무중단" 은 관측 해상도에 달려 있다.** 1초 간격·3초 타임아웃으로는
+> 44회 중 1회가 걸렸다. **더 촘촘히 보면 더 보일 것이다.**
+
+---
+
+## 3. B-4 ③ — role 변경은 언제 반영되는가
+
+### 왜 못 했었나
+
+B-4 를 할 때 **oauth2-proxy 가 아직 배포되지 않아** "proxy session" 이
+존재하지 않았다. B-7 에서 배포했으므로 이제 측정할 수 있다.
+
+### 방법
+
+`X-Auth-Request-Roles` 대신 **이미 전달되고 있는 `x-forwarded-email`** 을 썼다.
+role 을 헤더로 내보내려면 추가 설정이 필요한데, **"IdP 의 클레임 변경이
+언제 반영되는가" 라는 질문은 어느 클레임이든 같다.**
+
+```bash
+# 1. 기준선
+fetch('/api/echo') → x-forwarded-email = labuser@example.com
+
+# 2. IdP 에서 바꾼다
+kcadm.sh update users/ -r keycloak-patterns -s email=CHANGED-labuser@example.com
+
+# 3. 반복 요청
+for (i=1..12) fetch('/api/echo') # 0.5초 간격
+```
+
+### 결과 — 반영되지 않는다
+
+```
+ 1: labuser@example.com
+ 2: labuser@example.com
+ ...
+12: labuser@example.com ← 12회 · 6초 동안 옛 값
+```
+
+세션을 지우고 재인증시키자
+
+```
+재인증 후 email = changed-labuser@example.com
+```
+
+### 개념 — 세션은 로그인 시점의 스냅샷이다
+
+```
+ 로그인 → IdP 가 준 클레임을 세션에 담는다
+ 이후 요청 → 세션에서 읽어 헤더로 내보낸다
+ └─ IdP 를 다시 부르지 않는다
+ IdP 에서 변경 → 세션은 모른다
+```
+
+**oauth2-proxy 에 `--cookie-refresh` 가 설정되어 있지 않다.**
+설정하면 그 주기마다 토큰을 갱신하며 클레임을 다시 받는다.
+
+| 설정 | 반영 시점 |
+|---|---|
+| 지금 (`--cookie-refresh` 없음) | **쿠키 만료(1시간) 또는 재인증까지 안 됨** |
+| `--cookie-refresh=5m` | 최대 5분 |
+
+> **Q4 는 "몇 번째 요청부터 반영되는지" 를 물었는데, 답은 "요청으로는 안 된다" 이다.**
+> 요청 횟수가 아니라 **세션의 나이**가 정한다.
+>
+> **이것이 Q4 의 설계 판단 2번(role·tenant 변경이 즉시 반영돼야 하는가)에
+> 직접 답한다** — 즉시가 필요하면 헤더 방식은 맞지 않는다.
+
+---
+
+## 4. B층에 Grafana 증거가 없는 이유
+
+문서 감사에서 **B-1·B-3·B-4·B-5 에 스크린샷이 없는 것**이 드러나
+소급해서 찍으려다 원인을 확인했다.
+
+```
+=== Prometheus 가 실제로 긁는 대상 ===
+ keycloak 2개
+ kubelet 2개
+ node-exporter 2개
+ prometheus 1개
+
+=== B층 구성 요소의 지표가 있는가 ===
+ redis_up 시계열 0개
+ redis_connected_clients 시계열 0개
+ pg_up 시계열 0개
+ pg_stat_database_numbackends 시계열 0개
+```
+
+**Redis 도 PostgreSQL 도 BFF 도 긁는 대상에 없다.**
+
+> **스크린샷을 안 찍은 것이 아니라 지표가 없다.**
+> A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 `/metrics` 를
+> 내놓고 그것을 scrape 대상에 넣어뒀기 때문이다.
+>
+> **관측은 "나중에 붙이는 것" 이 아니라 실험 설계에 포함되어야 한다.**
+> A-2 에서 `kube-state-metrics` 가 없다는 것을, A-6 에서 응답 시간
+> 히스토그램이 없다는 것을 찾았는데, **B층 전체가 빠져 있던 것은
+> 문서 감사를 하고서야 드러났다.**
+
+### 보완하려면
+
+| 대상 | 방법 |
+|---|---|
+| Redis | `redis_exporter` 사이드카 또는 Deployment |
+| PostgreSQL | `postgres_exporter` |
+| BFF | 이미 actuator 가 있다 — `/actuator/prometheus` 노출 + scrape 추가 |
+| 파드 readiness | `kube-state-metrics` (A-2 에서 이미 찾은 항목) |
+
+---
+
+## 5. 남은 것 — D-4 강제 갱신
+
+```
+$ sudo -n -l
+sudo: a password is required
+```
+
+**호스트 sudo 가 비밀번호를 요구해 `certbot renew --force-renewal` 과
+`systemctl reload nginx` 를 실행할 수 없다.** 사람이 함께 있어야 한다.
+
+측정 계획은 준비되어 있다.
+
+```bash
+# 측정 쪽 (내가 실행)
+while true; do
+ printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 https://auth.hyeonworks.com/realms/master)"
+ sleep 0.2
+done
+
+# 주입 쪽 (사람이 실행)
+sudo certbot renew --force-renewal
+```
+
+**0.2초 간격으로 재는 이유** — 2절에서 1초 간격으로는 전환을 거의 못 잡았다.
+nginx reload 는 그보다 훨씬 짧을 것이므로 해상도를 올려야 한다.