diff --git a/.gitignore b/.gitignore
index 44adaca7..32330e3a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,4 @@ secrets
 
 # Ignore generated OpenAPI code
 /pkg/api/openapi/
+/data/generated/
diff --git a/Makefile b/Makefile
index 9d93710d..cf5b9b6e 100755
--- a/Makefile
+++ b/Makefile
@@ -211,6 +211,7 @@ test-integration: install secrets
 # Regenerate openapi client and models
 generate:
 	rm -rf pkg/api/openapi
+	mkdir -p data/generated/openapi
 	$(container_tool) build -t hyperfleet-openapi -f Dockerfile.openapi .
 	$(eval OPENAPI_IMAGE_ID=`$(container_tool) create -t hyperfleet-openapi -f Dockerfile.openapi .`)
 	$(container_tool) cp $(OPENAPI_IMAGE_ID):/local/pkg/api/openapi ./pkg/api/openapi
@@ -220,6 +221,7 @@ generate:
 # Regenerate openapi client and models using vendor (avoids downloading dependencies)
 generate-vendor:
 	rm -rf pkg/api/openapi
+	mkdir -p data/generated/openapi
 	$(container_tool) build -t hyperfleet-openapi-vendor -f Dockerfile.openapi.vendor .
 	$(eval OPENAPI_IMAGE_ID=`$(container_tool) create -t hyperfleet-openapi-vendor -f Dockerfile.openapi.vendor .`)
 	$(container_tool) cp $(OPENAPI_IMAGE_ID):/local/pkg/api/openapi ./pkg/api/openapi
diff --git a/data/generated/openapi/openapi.go b/data/generated/openapi/openapi.go
deleted file mode 100755
index b20c9c24..00000000
--- a/data/generated/openapi/openapi.go
+++ /dev/null
@@ -1,244 +0,0 @@
-// Code generated for package openapi by go-bindata DO NOT EDIT. (@generated)
-// sources:
-// ../../openapi/openapi.yaml
-package openapi
-
-import (
-	"bytes"
-	"compress/gzip"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"time"
-)
-
-func bindataRead(data []byte, name string) ([]byte, error) {
-	gz, err := gzip.NewReader(bytes.NewBuffer(data))
-	if err != nil {
-		return nil, fmt.Errorf("Read %q: %v", name, err)
-	}
-
-	var buf bytes.Buffer
-	_, err = io.Copy(&buf, gz)
-	clErr := gz.Close()
-
-	if err != nil {
-		return nil, fmt.Errorf("Read %q: %v", name, err)
-	}
-	if clErr != nil {
-		return nil, err
-	}
-
-	return buf.Bytes(), nil
-}
-
-type asset struct {
-	bytes []byte
-	info  os.FileInfo
-}
-
-type bindataFileInfo struct {
-	name    string
-	size    int64
-	mode    os.FileMode
-	modTime time.Time
-}
-
-// Name return file name
-func (fi bindataFileInfo) Name() string {
-	return fi.name
-}
-
-// Size return file size
-func (fi bindataFileInfo) Size() int64 {
-	return fi.size
-}
-
-// Mode return file mode
-func (fi bindataFileInfo) Mode() os.FileMode {
-	return fi.mode
-}
-
-// Mode return file modify time
-func (fi bindataFileInfo) ModTime() time.Time {
-	return fi.modTime
-}
-
-// IsDir return file whether a directory
-func (fi bindataFileInfo) IsDir() bool {
-	return fi.mode&os.ModeDir != 0
-}
-
-// Sys return file is sys mode
-func (fi bindataFileInfo) Sys() interface{} {
-	return nil
-}
-
-var _openapiYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5d\x7b\x73\xdb\xb6\x96\xff\xdf\x9f\xe2\x4c\xf7\xce\x38\xe9\x46\x92\x9d\x74\x76\xb6\x9a\xe9\xcc\xe6\xb1\xbd\xf5\x6e\xea\x64\x63\xb7\x9d\xd9\x3b\x77\x63\x88\x3c\x32\x91\x80\x00\x0b\x80\x72\x74\xbb\xfd\xee\x77\xf0\x22\x41\x8a\x94\x28\x59\x79\x38\x96\xa7\x7f\x54\x20\x1e\x07\x07\x38\xbf\xf3\xc3\xc1\x23\xa2\x40\x4e\x0a\x3a\x85\x27\xe3\x93\xf1\xc9\x11\xe5\x73\x31\x3d\x02\xd0\x54\x33\x9c\xc2\x4f\xcb\x02\xe5\x8f\x0c\x51\xc3\xd3\xd7\x67\x47\x00\x0b\x94\x8a\x0a\x3e\x85\x53\x9b\x1d\x20\x11\x5c\x93\x44\x9b\x32\x00\x9c\xe4\xcd\x42\x97\x48\xf2\x23\x80\x14\x55\x22\x69\xa1\x6d\xc9\xff\x1f\xd9\xbc\xcd\xaa\xa1\x90\x62\x41\x53\x54\xa0\x68\x5e\x30\x84\xe7\x6f\x7e\x79\x01\xa2\x40\x49\x4c\x29\x05\x73\x21\x21\x27\x9c\x5c\x53\x7e\x0d\x09\x2b\x95\x46\x09\x12\x95\x28\x65\x82\x0a\x08\x4f\x41\x67\x48\x25\x28\x4d\x74\xa9\x20\xa3\x4a\x0b\xb9\x1c\x1f\xd9\xc6\xbe\xfd\xf6\xa9\x4c\x32\xaa\x31\xd1\xa5\xc4\x6f\xbf\x9d\xc2\x45\xdc\x0c\x67\xcb\x47\xc0\x05\xcc\x4a\x45\x39\x2a\x05\x4c\x5c\xd3\xc4\x26\xe1\x02\xb9\x86\x44\xa2\x95\x63\x6c\x6b\xbb\x40\xae\x29\x47\xe6\xe5\x13\x12\x32\xc2\x53\x66\xc4\x60\x0c\x84\x4c\x32\x54\xda\x09\xee\x6a\x72\xc5\x9e\xa6\xa4\xd0\x28\x95\xcf\x6d\xe4\x05\x55\x60\x42\xe7\x34\x51\x20\xe6\x75\xff\x4c\xea\x91\x26\xd7\x6a\x0a\x7f\xfb\xfb\x51\x41\x74\xa6\x8c\x82\x27\xa4\xa0\x93\xcc\xe8\x6d\x6e\xf4\x36\x59\x9c\x4e\xbc\x26\x94\xd3\xff\x35\xfa\x81\x80\x5a\x75\x67\xe9\xd4\xa4\x3f\xf7\x19\xfd\x67\x55\xe6\x39\x91\xcb\x29\xbc\xa4\x4a\x07\x7d\x86\x8f\x05\x91\x24\xc7\xba\x5a\xf3\x37\x82\xbf\x48\x9c\x4f\xe1\xf8\x5f\x26\x89\xc8\x0b\xc1\x91\x6b\x35\xa9\x73\x4e\xfe\xa7\x44\xb9\x7c\x6d\x7e\xab\x71\x41\xae\xf1\x78\xf7\xa2\x17\xf4\x1f\xbb\x16\x17\x32\x45\xf9\x6c\x79\x9b\xd2\x5b\x94\xbd\x40\x22\x93\xcc\x15\x0e\xc5\x24\xaa\x42\x70\x85\x91\xee\x8e\x1f\x9f\x9c\x1c\xd7\x3f\x5b\xf6\x70\x99\x21\x48\xfc\xbd\x44\xa5\x21\x23\x0a\x54\x99\x24\x88\x29\xa6\xe3\xa8\x84\x31\x33\xe4\x3a\xae\x04\x80\x14\x05\xa3\x89\x1d\xe5\xc9\x3b\x25\x78\xf3\x2b\x80\x4a\x32\xcc\x49\x3b\x15\x3a\x7b\xe5\xf2\xaa\x89\x9f\x27\x66\x5a\xd4\x8a\x48\x71\x4e\x4a\xa6\x7b\xbb\xf0\x94\x43\xc9\xf1\x43\x81\x89\xc6\x14\x50\x4a\x21\x2b\x3d\x7c\x96\x5e\xfc\xa7\x11\xc1\xc9\x5f\x08\xd5\x6d\x14\xe6\x83\xef\x6d\xdb\x28\x9e\x1b\x73\xc7\x60\x16\x47\x1d\x3d\xf6\x20\x66\xfe\x7c\x66\x02\x1c\x6f\x56\x90\xc9\xe3\x0f\x58\x0c\x3a\x17\xda\x62\x8f\x19\xf1\x2b\x87\x53\x57\x20\x66\xef\x30\xd1\x40\xb9\xc5\x83\xa0\x35\xa0\x0a\x24\x92\x74\x64\xb0\xc9\xa2\x9b\xe9\x65\x69\xd4\x3b\x5b\x3a\xe4\x40\xb9\xa0\x49\xa4\xde\x33\x6d\x0a\x9d\xbf\xba\x34\xf6\xab\x0d\x9e\xe8\x68\x66\xcd\x44\xba\x1c\xc3\x19\xa7\x9a\x12\x66\xe0\xce\xb5\x3f\x2e\x32\xa2\x10\x6e\x28\x63\x30\x43\xf8\xe6\x5c\xe8\x37\x48\xd2\xe5\x37\xa6\xcd\xaa\x6a\x9f\x97\x04\x00\x0b\xd9\x31\x2f\xf4\x12\x4a\xae\x29\x83\xea\xe3\xeb\x57\x17\x97\x0d\x2c\x1e\xaf\xc2\x8a\x81\x35\x97\xd8\x6d\x2e\xa7\x3b\x98\x8b\xd5\x92\x1b\x85\xa0\x7d\x9b\x61\x86\xc8\x1d\x80\x9b\x3c\x0a\x88\xf9\x5c\x32\xfd\x39\xcd\xeb\xeb\x30\x2d\x08\x03\xf1\x4c\xa4\xcb\xba\x16\x93\x48\x25\xa6\x53\xd0\xb2\xc4\xa3\x35\x12\xae\x97\xaf\x5b\xba\x01\xda\x75\x16\xf9\xc6\xc9\x76\xbc\xde\x71\x4e\xfe\xf0\xff\xf7\x96\xa6\x7f\x0e\xf5\xa2\xcf\x96\x67\x69\x1b\x33\xfe\x8a\x95\x1f\x35\x26\x7a\xf6\xe2\x36\xce\xb4\xcb\xb1\x98\x62\x8e\x60\xd5\x12\x47\xba\xa1\x7c\x0a\x86\x29\x44\x49\x3d\x03\xd1\xad\x59\xbd\x2c\x70\x0a\x4a\x4b\xca\xaf\xbf\x2a\x57\xf6\x35\xd8\xda\xd0\x09\x3c\xe1\x22\xc5\x42\x08\xb6\x99\x10\x9e\x8b\x14\x5f\x9b\x9c\xcf\x96\x5e\x51\xab\x73\xda\x92\x43\xc3\x69\xab\x6a\x2d\x0f\x5f\xe3\x17\xdf\xa0\x2e\x25\x57\xd6\xf5\x30\x53\x5a\xcc\x3b\x2a\x20\xad\x2a\xba\x6d\x64\x4f\x93\xbd\x21\x9f\xef\x6a\x6d\x9e\x7d\x43\xd3\x61\x0e\x07\x12\x7c\x5f\x48\x70\xb0\x8d\x7b\xc0\x82\x1d\x31\x0a\x1d\xee\x21\xc2\xc1\x7a\xbb\x2c\xbe\xa2\xbf\xa1\x8e\x3b\x6c\xe1\xf7\x8c\x8c\x86\x11\x0b\x7c\xc9\x75\xfe\xeb\x98\xf0\x5f\x1c\x37\x6d\x2b\x7b\x6b\x72\x5a\xfb\xf6\xc9\x1f\xe1\x7f\x07\x91\xd6\xd0\x72\x1f\x6b\x0d\x95\x35\x68\x6b\xa7\x57\x0f\xd1\xaa\x36\x20\xdc\x0d\xeb\xae\x65\x8a\xd4\xb7\x0f\xa1\x2a\xe4\xdb\x1f\xe6\xdc\x39\x57\xf9\x35\xa0\xc6\x6d\x2d\x71\xe2\xe2\x1d\xb8\x05\xf9\xbe\xf0\x25\x7a\x99\xb7\x0f\xab\x40\xa8\xda\x7a\xd7\xaa\xfd\x75\xc6\x9a\x94\x52\x22\xd7\x21\x20\xee\xe3\x4c\xa6\x38\x92\x24\xab\x2a\xd6\x19\x71\xf3\x48\x62\x21\xa4\x19\x12\x57\xe0\xde\x9a\xf6\x61\x35\x70\x58\x0d\x34\xf0\xc1\xef\xdb\x38\x53\xbd\x07\x4b\x02\xf3\x21\x00\x54\x1f\x3e\x05\xd6\xdf\x40\xa7\x2e\x34\x8a\x02\xe5\x5e\x8f\x9e\xfe\x2a\xa0\x5a\x01\x75\x01\xe9\x0e\x90\xd2\x19\x55\x01\x61\x6a\x55\x05\x70\xfb\xee\xe4\x7b\xa0\xf3\xaa\x7d\xc2\x24\x92\x74\x69\x27\x0b\xd9\x50\x19\x1c\x45\xb5\x85\x68\x3b\x4f\x58\x99\xa2\x8b\x5b\xf8\xe2\xd4\x91\xf8\x4c\xe2\xdc\xd6\x31\x2f\x75\x29\x31\xda\x93\x1c\xaf\xf4\x4c\x65\xa2\x64\x06\x40\x85\x44\x1f\x7d\x37\xe2\x62\x0a\x34\x9d\xd8\x8a\xb4\x80\xb2\x48\x8d\xea\x4c\xef\x7d\x4b\x8c\x44\x7d\x3c\x00\x6e\x4f\x93\xf7\x6c\x71\xd6\x40\x9d\x1a\x71\x8e\xbf\xdb\x04\x9c\x0a\xe5\xc2\x58\x99\x9d\x8a\x5c\x68\x28\x79\x8a\x52\x69\xbf\x45\x5e\xa9\x24\x2d\xd1\x4c\x47\xca\x17\x84\xd1\x14\xd4\x92\x6b\xf2\x61\x1c\x37\xf4\xdd\xb0\x86\x08\x37\xad\xcc\x69\xb3\x7e\x4c\xa3\x4d\xb0\xa8\xd2\xef\x87\x0d\x58\x22\xf8\x9c\xd1\x44\x2b\xb8\xa1\x3a\xb3\x15\xc7\x84\x06\xc3\xfe\x96\x13\x62\xfc\x65\xae\xf9\x1a\x63\xb8\xfb\xc2\x6f\x30\xad\xf4\x16\xbd\x1b\xa9\x1c\x10\xd5\xbd\x6f\x94\xf2\xc0\xef\xee\x39\xbf\xdb\x3b\x08\x0e\x39\x8d\x70\xe0\x5c\x07\xce\x75\x20\x40\x07\x02\x74\x2f\x09\x90\xc8\x0b\xa2\xe9\x8c\x32\xaa\x97\x9b\xf9\x4e\x9c\x7b\x1d\x71\xe9\xdd\x8e\x5e\x0b\x00\x07\xf7\xff\xa5\xb9\x7f\x8d\x1f\xf4\xa4\x60\x84\x0e\x46\x98\x0e\x1c\x55\x98\x94\xb2\x9a\x5e\xae\xdb\xcf\x90\x48\x94\x4f\x4b\x9d\xf9\xb3\x69\x1d\x73\x73\x87\xb3\x15\xbd\x0c\x7c\x7f\x07\x2a\x0e\x33\xf8\x4e\xcd\xe0\xc3\x71\x85\x4d\xb1\xc9\xfa\x8b\x29\xde\x9e\xd7\x2b\x43\x1f\xda\x70\x6c\xcd\x26\xf9\x14\x43\xd2\x7e\x37\xd9\x23\xdf\xe8\xdc\xe0\x9c\x30\x15\xfc\x60\x5b\xd2\x75\x12\xbe\x32\xb5\xbf\xa0\x12\x13\xd3\xf1\x55\x55\x5b\x05\xfb\x54\xfc\x50\x30\x91\x62\xdc\x58\xc7\xa4\xef\x90\xfe\xd9\xf2\x56\xf2\x77\x2e\x65\x2b\x01\x3d\xb9\x7b\x4b\xf4\x30\x31\x8d\x69\x37\x65\x34\x29\x7b\x10\x90\x72\x8d\xd7\xd5\x50\x81\x81\xc1\x9c\x68\x9b\xfe\xe4\xf1\xaa\xdc\xa7\xc3\xc5\x35\x48\xb4\x2a\xb2\x49\xfd\xf4\x62\x3f\x3e\xe9\x97\x3b\x06\xa2\xa6\xbc\xca\x7e\xd9\xf7\x2c\x58\x11\xc1\xcf\x6a\x57\xe8\xe9\xeb\xb3\x37\x9e\xad\x86\x5a\x5c\x1d\x6e\x45\x17\xbc\x8c\x34\xae\x4e\xd3\x18\x29\x19\x99\x21\x53\xb1\xa9\x77\x14\x34\x7f\x24\x4d\xa9\xb1\x1b\xc2\x5e\x77\x54\xd3\x3b\x77\x5b\xb0\xe5\x9a\xf3\x2b\x4c\xb4\x77\x87\xaa\x35\x0c\x51\x50\x10\x2a\xed\x65\x1a\xab\xca\x05\x61\x25\xfa\x0a\x83\xa7\x24\x8c\xbd\x9a\x6f\x72\x92\x95\xc5\xdb\x4e\xbc\xc1\x39\x4a\xe4\x89\xf7\x70\x0d\x56\xbb\x46\x5b\xd5\x50\x45\x8d\xf9\xd5\x73\x94\x22\x66\x96\xc2\xa7\x6f\xaf\x91\x7b\x1a\x11\x7d\x4d\x04\x77\x5a\x53\x6b\x86\xc0\x57\xba\x3a\x06\xeb\x55\x79\x4e\xf2\x6a\x19\xd1\x88\xdd\x79\x49\x30\x75\x8b\x78\xbf\x64\x7e\xf0\xab\x59\x27\x11\x2d\xe4\x23\x78\x71\x7e\x31\x1e\x8f\x1f\x56\x35\x77\x74\x62\x55\x9a\xb6\xf1\xf4\x99\x4f\x4b\xcc\xdf\x32\x9a\x64\x50\x57\xec\x0e\x98\x71\x40\xae\xa9\x5e\xc2\x83\x70\xb5\xe9\x51\x75\x10\x43\x3d\x84\x1b\x52\x87\x2b\x89\xb6\x5d\xd4\xd4\x75\xd7\x5d\xe6\xe2\xd7\x71\xe7\xe2\x45\x91\xd7\xf7\xaa\xfc\x44\x4a\xb2\x8c\x52\xa9\xc6\xbc\x35\x83\xd7\x9e\x47\x0e\x55\x1f\xf7\xf5\xf4\xbf\xcb\x19\x4a\x8e\x1a\xd5\x48\xe9\x25\xc3\x48\x1a\xd0\x92\x24\xef\x8d\xd4\x71\xfc\xa7\x5e\xcc\xa5\x44\x93\x5d\x6d\x10\xfe\xf8\xb3\x97\x24\xb8\xc6\x46\xd5\xc9\x1f\xd3\x0e\x3c\x50\x5a\x96\xf6\x2a\x1d\x2c\x88\xa4\xa8\x60\xb6\xac\x67\xd0\xb2\xc0\x7a\x62\xe4\xa8\xc9\x40\xd9\xba\x66\xb6\xf9\x7b\x27\x66\x6f\xad\x35\xb7\xd8\x44\xcf\x14\xaf\x0b\xa8\x82\x24\xc3\x4b\x11\xad\x31\x2f\x74\x77\xfe\xd5\xa9\x0b\x6b\xa6\xaf\xbd\x31\x23\x9d\x9f\x1d\xdc\xbe\x99\x31\x0c\xb7\x2c\x94\x96\xab\xc6\xb6\xa6\x40\x63\x6c\x7f\xf6\x43\x03\x64\x26\x4a\xdd\x40\x81\x77\x62\x36\xc1\x0f\x98\x94\x11\x1a\xe1\x07\x62\x04\x5c\x45\x1d\x58\x04\x5c\x58\x0b\x07\x95\x0f\xef\x33\xb2\xd1\xba\x1a\xeb\x4e\x3d\x5d\x10\xca\xc8\x8c\x61\x5b\xe1\x06\x8c\xe1\xf8\x52\x96\x78\xdc\xf8\x24\x91\x28\x3b\x97\x19\x0b\x15\x5b\x8b\x2a\x88\x52\x98\x36\xb2\xe6\xa8\x94\x61\x3c\x36\xef\x93\x93\x28\x3b\x68\x54\xba\xb3\xcc\x86\xbe\xda\xfe\x56\xac\x6b\x6a\x16\x16\x8f\x4f\x47\x27\xe6\xbf\xcb\x93\x93\xa9\xfd\xef\x7f\x9b\x02\xbb\x68\xe5\xc0\xec\x43\xb5\x66\x08\x7b\x4b\xf2\x01\x3a\xfb\xb5\x56\xc0\x3b\x31\x73\xb4\xbf\x4f\x67\xdd\x79\xdd\xda\x48\xa9\x79\xc9\xd8\xf2\x0e\x2a\xee\x27\x24\xac\x11\xe8\xdd\x45\x6f\x99\xad\x64\x39\x48\x6f\x54\x75\x66\xff\xac\xda\x6a\x23\xb8\xbf\xad\x2d\xbd\x13\x59\x9e\x4e\xab\xa4\x5f\x0d\xe5\x3a\x5d\x93\xf7\x71\x2b\xef\xe3\xb5\xbe\xa2\x02\xff\x7a\x90\x46\xef\xc4\xac\x23\x87\x43\xfb\xc0\xbd\x63\x9f\xe7\x81\xbd\xa1\xad\x08\xa0\x37\xeb\xaa\x01\xcd\x9b\xb3\x57\xa0\x0c\xa7\x27\x6a\x95\x31\x36\xe2\xa0\x77\x98\x3e\x86\x1d\x18\x9a\x1a\x26\x36\xa7\xb8\x1e\xff\xf7\x4b\x07\xc3\xc6\x52\x44\x0b\x63\xae\x2a\x71\xce\x30\xd1\x9f\x8b\xd7\x7d\x4a\x36\xb6\x0b\xd5\xda\xb6\xe1\xff\x12\x33\xa8\xf8\x40\xd5\xd0\xea\xd4\x7e\x49\xb7\x9e\xd1\x56\xd9\x6b\x26\xe7\xca\x60\xec\x65\xcc\x3a\x37\x97\xb6\x5a\x19\xd6\xc1\x36\xbf\xfe\xd8\xb2\xdf\x2b\x91\x18\x93\x58\x43\x72\x47\xce\xd9\xb2\x23\x67\x23\xb1\x13\x02\x9a\x67\x2f\x3a\x34\x1c\xb9\x8d\xcd\x08\x10\x4c\xd4\xb4\x3e\x32\x2b\xaa\xea\x5b\xe4\x4e\x6e\x53\x4d\xdd\xdb\x2d\xaa\xc1\x9c\x50\xb6\x22\xc9\x2d\xaa\xd8\x0f\x6e\xe5\x94\xd3\xbc\xcc\x9b\x9e\xa7\x61\x58\x7f\x8d\x56\xb5\x14\x59\x6a\xbc\xbf\x17\x1f\x04\x87\xa4\x54\x5a\xe4\x28\x7d\x9a\x7a\x14\x80\xcd\x2d\x5f\x31\xbc\xc7\x12\x56\xf1\xdf\x50\x1b\xab\xfd\x26\xfc\x0e\xe5\xab\xe6\x55\x23\x66\x01\xc3\x6e\xd0\xde\xc2\x4a\xc2\x85\x69\x52\xdf\x2b\x5a\x59\x47\xbc\xa7\x3c\xad\x76\xc6\x6b\xbb\x4f\xab\xbd\xf7\xd1\xe9\xe3\x27\x55\x7a\x66\x5b\xcb\xb4\x2e\xd4\x74\x32\x21\x05\x1d\xd7\xbb\x34\xe3\x44\xe4\x8d\x63\x54\x5d\x15\x34\xb6\xf5\x1b\x5f\x56\x23\x59\xc8\x17\x54\x0a\x9e\x23\xd7\x96\xb5\xa4\x65\xd2\xb0\x2e\x00\x8d\x24\x9f\x42\xc1\x88\x36\x93\xa0\xd6\x73\x81\x49\x03\x50\xb7\xe0\x66\x5b\xf0\xb2\x6e\x2e\xb8\x3a\xc8\xf6\x95\x87\x29\xd8\xa7\x1d\xa2\x64\x46\x94\x7e\xab\x25\xe1\xca\x7a\x83\xb7\xc6\x10\x07\xf0\x9b\x8d\x54\x74\x2b\x1e\x1e\x1e\x8e\x68\x82\xf7\x46\x76\xbe\x7e\x39\xb8\x96\xa4\x6f\xb5\x24\xdc\x6d\x51\x38\x88\xaf\x6f\xcd\xd8\xb7\xd4\x6c\x43\x8b\x29\x57\xfb\xd7\xdf\x8b\xf3\x0b\x90\x98\x08\x99\xaa\xd0\x97\x3e\xe5\x39\x28\x1a\xa7\x22\x27\x94\x1b\x43\xed\x29\xf0\xd9\xf5\x16\x39\x20\x28\x95\x83\x88\xff\xf0\xa0\x65\xe4\xee\x72\x33\xfd\x19\x23\x00\xdc\x92\x20\x18\x58\x8c\x7e\x1a\xd8\x8a\xbd\x7a\x81\xc9\x1a\x9f\x6e\x31\x75\x08\x9f\xf7\x1b\x15\x6d\xf4\x6d\xc7\xdb\x7a\xca\xe7\x94\xbf\x44\x7e\xad\xb3\xe6\x10\xe5\xe4\x43\x48\xfe\xb7\x27\x31\x0c\x99\xa5\x98\xe4\x53\xf8\xbf\xbf\x91\xd1\x3f\x4e\x46\xdf\x8f\xfe\xfe\xaf\x7f\xe9\x73\x8d\xe1\xa8\x94\x11\x05\x1e\x94\x9c\xfe\x5e\x46\x71\x45\x0b\xb0\x31\x8a\x34\xbd\x12\x0c\xf5\x4c\x17\x05\x26\xcf\x85\xc4\xde\x98\x6c\x74\x98\x2e\x1a\xcf\xea\x2a\x24\x69\xf9\x03\x80\xe7\x2f\x7f\x86\x54\xa0\xe2\xc7\x1a\xde\x73\x71\x03\x99\xb8\xb1\x67\xd0\x78\x4e\xa4\xca\x08\x63\xd5\xcb\x5f\x8f\x80\x6a\xfb\xfa\x98\x3b\xbe\xe6\x9e\x31\x9b\x0b\x79\x43\x8c\x41\x69\x51\x3f\xaa\xa3\x05\xa4\xc2\xbf\xaa\xd3\x5c\xfe\x02\x3c\x2b\xb5\x6d\xd4\xbe\xca\xe3\xc1\xc9\xbf\x2e\x66\xfb\x0a\x33\x9c\x0b\x89\x40\x92\x04\x8b\x8a\x39\xf8\xed\xeb\x5d\x9c\x7a\xb4\x57\xd4\x60\xc0\x43\x17\xb6\x9f\x88\x41\x7c\x7a\x47\x1f\x3d\x9e\x75\x27\x56\x41\xad\x17\x52\x6e\xb7\xfe\x09\x96\xb4\xa6\xe7\xbd\x4f\x68\x99\xe9\x99\x74\x59\x56\x74\xde\xd3\xce\x5e\x63\x22\xcb\x48\x43\x40\x54\x65\x4d\x86\x3b\x87\xc8\xd2\x88\x5c\x73\xa1\x74\x78\x7a\xcf\xfc\x5d\x66\x54\xb9\x67\xb5\x0a\x89\x0a\x79\x75\xc6\xd1\xbf\x38\xe8\x8f\xae\x1a\xab\x63\x4c\xdc\xc0\x9c\xe1\x07\x7f\xb4\x6c\xdc\xe8\xe7\x2e\xdb\x7e\x96\x80\x45\xbf\xbb\x98\x57\xf4\x79\x7d\x50\xa7\x73\x95\x18\xa0\x62\xcd\x24\x72\x2c\x70\x33\xaa\x23\x2f\xf3\x36\x94\x86\x87\xc1\x5a\xc9\x5d\x69\x3f\x12\xca\x1a\x6e\x7d\x2d\x9e\xfa\x3d\xba\x30\xfa\xee\x3d\xb2\x07\x9c\x68\xba\x40\x1b\xdf\x98\x99\x84\x44\xb0\x32\xe7\x0f\xc7\x8d\xb2\xbf\xf8\x85\xd2\x4d\x86\xbc\x86\x4a\x77\xe9\xa0\xf5\xf6\x98\xfb\x3b\x17\xda\xf6\x37\x7a\xf9\xac\x7a\x77\x92\x5c\x5f\x4b\xbc\xb6\xf5\x2d\x28\xde\xc0\x5c\x8a\xbc\x71\x59\xa2\x8e\x20\x8d\x23\x20\xe9\x20\xd0\xb7\x59\xfd\xae\x57\xd6\x6f\xa6\xa3\x41\x53\xa6\x6d\xa8\xdb\xc6\x14\x1e\x84\x95\x63\xbd\x1b\xa7\x1e\x19\x6a\x62\x53\xaa\x57\x2b\xe7\x42\xc2\x8c\x24\xef\xc5\x7c\xfe\x70\x5b\x7d\x02\x75\x0b\x4b\xa7\xbc\x24\x23\xfc\x1a\x6b\x5e\xf9\x31\xc3\x7e\x2d\x55\xbc\x34\xbd\x8f\x82\x7f\x85\x14\x09\x2a\xd5\xec\xfc\xed\x66\x8b\x05\x8c\xf0\xd4\x9d\x3d\x7f\x27\x6e\x50\x69\x70\x87\x0a\xc4\xbc\x71\xe9\xe5\x58\x75\xf5\xde\xe5\x55\xad\x6a\x83\xfa\x5c\x45\x54\x81\x99\xe2\x76\xbd\x6f\x43\x98\x1d\x36\xbf\xa7\xd8\xca\x7a\x95\x5e\xd2\xfa\x38\x80\x9d\x5c\x21\xec\x1d\x94\xe4\x4c\x62\x2f\xba\x7d\xc5\x52\xa3\xca\xab\xba\x63\x57\xae\xfa\x78\x17\x72\x25\x78\xdd\xbd\x58\xdc\x6f\x10\xb7\x5a\x0f\x1d\x6f\x72\x5a\xc1\x5f\x39\xc3\x08\xf8\x61\x43\x39\x2d\xec\x50\xd1\x33\x90\x76\x56\x85\x47\x1f\x55\xdf\xb3\x8e\x96\x09\x3e\x7f\x7a\x7e\xfe\xea\xd2\xcc\xbe\x5c\xa4\x74\x4e\x31\x85\xd4\x1e\x3e\x63\xcb\xf6\xb3\x8f\x11\x7a\xc5\xa3\x14\x84\xf3\xe1\x23\x7b\xcb\x04\x53\xe3\xe3\xae\xa2\xbb\x66\xf1\x1d\xb3\xab\x48\xd4\xd7\x01\x1b\x7f\x2f\x69\xf2\x1e\xc4\xc2\xc8\x86\x37\x66\x8e\xdc\xd8\x20\x3c\xa9\x5f\xb4\x5d\x60\x7d\xdb\xcb\xde\x2e\xa8\x05\xb2\x93\xdd\xbb\xd0\xa0\xe5\x5b\x6f\x7b\x98\x82\xf1\x72\xa8\x79\x8a\x63\xe3\xb6\xc8\xa0\xe0\xeb\x47\xd8\x2b\x69\xac\xe2\x6c\xc1\x4d\x15\xad\x06\x73\xb6\xf0\xda\x1d\x8b\xf7\x11\x1c\xff\x48\x98\x5a\x49\xfd\x85\x9b\xc5\x0a\xef\x5d\x8d\x85\x91\x6b\xab\xda\x87\x02\xb6\xd4\xc7\xcf\x24\xc9\x28\xc7\x91\x44\x92\x1a\x63\xf3\xd5\x40\x22\xd2\x5a\x3f\x21\x76\xb0\x65\xdd\x3f\x95\x39\xe1\x75\xcd\xbe\x96\x8f\xe3\xa4\xf6\x14\x3b\x1f\x00\xcc\x16\x5a\x6d\x4c\xb7\x1a\x09\x7b\xc6\xa9\x23\x88\x32\x82\x33\x0e\xa4\x02\x62\x6f\x98\xf6\xe1\xec\xfa\x3c\x51\x04\xbd\x3f\xfc\x50\x77\xe3\x6a\xb5\x26\x7f\xcc\xee\x11\x5c\x45\xd9\xaa\x25\xae\xe3\x00\xee\x92\x0a\xc7\x9b\x0a\x6a\xba\x9d\xc5\xc7\x71\x64\xeb\x14\xe4\x5b\xfc\x64\x94\xed\x37\x27\x86\x85\xf6\x20\x47\x07\x55\x8b\x71\x3f\x60\xbe\xed\x81\x47\x6c\x4f\xad\x1e\x36\x61\xb3\x72\x4e\x5b\xe2\x67\x84\x96\x5d\xa0\x36\x08\x88\xba\x11\xa6\x1d\x3e\xdc\x6e\x6d\x1f\xfa\x15\xaf\xee\xfb\x2c\xc1\xad\xbb\xaa\xf6\x8c\xd7\x8b\x6e\x0e\xd6\xca\x76\x13\x8f\x37\x4f\x34\x52\x55\xf1\xe0\xe7\x2f\x7f\x36\x0e\xd0\x0f\x40\xcd\x66\x6b\x97\x7a\x65\x7f\xff\x60\xaf\x37\x5e\x79\x4e\x13\x9c\xb9\xa8\x79\xcd\x55\x25\xcb\x0f\x5a\x96\x78\xd5\x1c\xaa\x1d\x22\x7e\x07\x2f\x77\xf0\x72\x83\xeb\x3e\x78\xb9\x83\x97\xeb\xf3\x72\xfd\x71\xb6\x3c\x17\xdc\x9d\xe1\x9d\x0b\xd9\xbc\xb4\xad\x1e\x85\x87\xe3\x63\x4c\x72\xa8\x5b\x9d\xb5\xb6\x71\xea\x14\x28\x87\x17\xcf\x1e\x46\xd9\x7c\xd0\x17\x0a\xb2\x64\x82\x38\x39\xec\x7d\x9e\x2d\x2f\x13\xd0\xcd\xbb\x16\x83\xb7\x36\x1a\xb7\xe8\xfc\x0d\x81\xc6\xa6\x8a\xdd\x4a\xde\xb5\xa6\x5f\xde\x9c\xd5\xf6\x21\xd2\xcd\xf6\x3b\x10\x42\xaa\xbb\x84\x6f\x37\xe8\x62\x87\x28\x73\x6b\x4b\xa9\x88\x01\x63\x04\xaa\xbe\x22\x63\x7d\x8f\xd0\x84\x45\xbf\x37\x85\xa8\x07\x8d\x4b\xd1\x89\x74\x43\x71\x48\x45\x57\x7b\xb6\x2f\x6d\x3b\xb4\x7b\xf1\xad\x42\xf0\x61\x47\x20\xdc\x48\xf8\x02\x0e\x07\x89\x1b\x8e\xf2\xad\x0c\x57\x59\x54\x1f\xaf\x38\x1c\x11\x1a\x56\x45\x5b\x9f\x43\x0f\xd5\x74\x5e\x29\x82\xad\xcf\xe6\x34\x5f\x68\xdb\x69\x0b\xa7\x7a\x18\xb6\x62\xdf\x71\xca\x96\x53\x76\xbb\xfd\xe9\xbb\x76\x69\x6c\x90\x67\xea\x73\x15\x1d\x07\x64\xbf\x40\x27\x36\x70\xcb\xbf\xfb\xe5\xdb\x78\x93\xde\xb0\x83\xea\xfd\xef\x3d\xee\xd8\x57\x33\xfe\xb0\x65\x5f\x5b\xea\x6d\x0e\x93\x1f\x4c\xf6\x60\xb2\x07\x93\xfd\x6c\x26\xeb\x9e\x5a\xb8\x43\xcc\x70\x7b\xcc\x38\xf0\xc6\x3b\xc2\x1b\x0f\xf0\x7e\x80\xf7\x03\xbc\xef\x05\xde\xef\xcc\xe9\xbf\xf6\x3f\xdd\xb0\xe3\xf1\xbf\xf6\x24\xd8\xf5\xfc\x5f\xf5\xcf\x90\x7c\xb1\x07\x00\x9b\xd0\x79\xcb\x13\x80\xeb\xb7\x6e\x36\x9c\x0f\xfc\x1a\x4f\x00\x56\xc8\x75\xb7\x8e\x00\xee\xf9\xcc\xdb\xe6\x2b\x63\xf7\xfa\x20\xdc\x27\xd9\xbd\xaf\x66\xe2\x7e\x8f\x58\x1e\x0e\xf1\xf9\xbf\x2f\xe8\x10\x5f\x35\xd4\x9f\xf1\x14\x5f\x8b\xd8\xef\x7f\xc7\xec\xee\xb2\xe0\xe6\x83\x7b\x4d\xcd\x34\x1f\x37\x6b\x38\xac\x11\x90\xea\x1d\x3e\xf3\xcb\x3f\xcb\x17\x1e\xfe\xbc\x30\xd3\x25\xe8\x30\x7a\xf9\xb3\x51\x7d\xa6\x75\xe1\x13\xec\xf4\xc2\xa9\xcf\x7a\xe4\x5e\xdc\xb5\xc5\x47\x50\x4a\xe6\xf2\x4e\x27\x13\x26\x12\xc2\x32\xa1\xf4\xf4\xdf\x4f\x4e\xdc\xcb\x6f\x8d\xee\xbd\xc0\x05\x32\x51\xe4\xc8\xdd\x80\x2e\x88\xa4\x66\x92\xba\xed\xa2\x7f\x06\x00\x00\xff\xff\x9d\xec\xe5\x93\x8a\x81\x00\x00")
-
-func openapiYamlBytes() ([]byte, error) {
-	return bindataRead(
-		_openapiYaml,
-		"openapi.yaml",
-	)
-}
-
-func openapiYaml() (*asset, error) {
-	bytes, err := openapiYamlBytes()
-	if err != nil {
-		return nil, err
-	}
-
-	info := bindataFileInfo{name: "openapi.yaml", size: 33162, mode: os.FileMode(420), modTime: time.Unix(1762937650, 0)}
-	a := &asset{bytes: bytes, info: info}
-	return a, nil
-}
-
-// Asset loads and returns the asset for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func Asset(name string) ([]byte, error) {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	if f, ok := _bindata[cannonicalName]; ok {
-		a, err := f()
-		if err != nil {
-			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
-		}
-		return a.bytes, nil
-	}
-	return nil, fmt.Errorf("Asset %s not found", name)
-}
-
-// MustAsset is like Asset but panics when Asset would return an error.
-// It simplifies safe initialization of global variables.
-func MustAsset(name string) []byte {
-	a, err := Asset(name)
-	if err != nil {
-		panic("asset: Asset(" + name + "): " + err.Error())
-	}
-
-	return a
-}
-
-// AssetInfo loads and returns the asset info for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func AssetInfo(name string) (os.FileInfo, error) {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	if f, ok := _bindata[cannonicalName]; ok {
-		a, err := f()
-		if err != nil {
-			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
-		}
-		return a.info, nil
-	}
-	return nil, fmt.Errorf("AssetInfo %s not found", name)
-}
-
-// AssetNames returns the names of the assets.
-func AssetNames() []string {
-	names := make([]string, 0, len(_bindata))
-	for name := range _bindata {
-		names = append(names, name)
-	}
-	return names
-}
-
-// _bindata is a table, holding each asset generator, mapped to its name.
-var _bindata = map[string]func() (*asset, error){
-	"openapi.yaml": openapiYaml,
-}
-
-// AssetDir returns the file names below a certain
-// directory embedded in the file by go-bindata.
-// For example if you run go-bindata on data/... and data contains the
-// following hierarchy:
-//     data/
-//       foo.txt
-//       img/
-//         a.png
-//         b.png
-// then AssetDir("data") would return []string{"foo.txt", "img"}
-// AssetDir("data/img") would return []string{"a.png", "b.png"}
-// AssetDir("foo.txt") and AssetDir("notexist") would return an error
-// AssetDir("") will return []string{"data"}.
-func AssetDir(name string) ([]string, error) {
-	node := _bintree
-	if len(name) != 0 {
-		cannonicalName := strings.Replace(name, "\\", "/", -1)
-		pathList := strings.Split(cannonicalName, "/")
-		for _, p := range pathList {
-			node = node.Children[p]
-			if node == nil {
-				return nil, fmt.Errorf("Asset %s not found", name)
-			}
-		}
-	}
-	if node.Func != nil {
-		return nil, fmt.Errorf("Asset %s not found", name)
-	}
-	rv := make([]string, 0, len(node.Children))
-	for childName := range node.Children {
-		rv = append(rv, childName)
-	}
-	return rv, nil
-}
-
-type bintree struct {
-	Func     func() (*asset, error)
-	Children map[string]*bintree
-}
-
-var _bintree = &bintree{nil, map[string]*bintree{
-	"openapi.yaml": &bintree{openapiYaml, map[string]*bintree{}},
-}}
-
-// RestoreAsset restores an asset under the given directory
-func RestoreAsset(dir, name string) error {
-	data, err := Asset(name)
-	if err != nil {
-		return err
-	}
-	info, err := AssetInfo(name)
-	if err != nil {
-		return err
-	}
-	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
-	if err != nil {
-		return err
-	}
-	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
-	if err != nil {
-		return err
-	}
-	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-// RestoreAssets restores an asset under the given directory recursively
-func RestoreAssets(dir, name string) error {
-	children, err := AssetDir(name)
-	// File
-	if err != nil {
-		return RestoreAsset(dir, name)
-	}
-	// Dir
-	for _, child := range children {
-		err = RestoreAssets(dir, filepath.Join(name, child))
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func _filePath(dir, name string) string {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
-}
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 58271875..5d32c172 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -43,7 +43,7 @@ paths:
 
         **Note**: The `status` object in the response is read-only and computed by the service.
         It is NOT part of the request body. Initially, status.phase will be "NotReady" and
-        status.adapters will be empty until adapters POST their status.
+        status.conditions will be empty until status conditions are POSTed.
       parameters: []
       responses:
         '201':
@@ -183,7 +183,7 @@ paths:
     get:
       operationId: getNodePoolsStatuses
       summary: List all adapter statuses for nodepools
-      description: Returns current status object for each adapter that has reported status
+      description: Returns adapter status reports for this nodepool
       parameters:
         - name: cluster_id
           in: path
@@ -216,13 +216,13 @@ paths:
                 $ref: '#/components/schemas/Error'
     post:
       operationId: postNodePoolStatuses
-      summary: Create adapter status
+      summary: Create or update adapter status
       description: |-
-        Adapter creates its initial status object for this cluster.
-        Returns 409 if adapter already has a status object for this cluster 
+        Adapter creates or updates its status report for this nodepool.
+        If adapter already has a status, it will be updated (upsert by adapter name).
 
-        Response includes the status id and href for future operations.
-        Adapter should store the returned id/href to update its status later.
+        Response includes the full adapter status with all conditions.
+        Adapter should call this endpoint every time it evaluates the nodepool.
       parameters:
         - name: cluster_id
           in: path
@@ -258,7 +258,7 @@ paths:
     get:
       operationId: getClusterStatuses
       summary: List all adapter statuses for cluster
-      description: Returns current status object for each adapter that has reported status
+      description: Returns adapter status reports for this cluster
       parameters:
         - name: cluster_id
           in: path
@@ -282,13 +282,13 @@ paths:
           description: The server cannot find the requested resource.
     post:
       operationId: postClusterStatuses
-      summary: Create adapter status
+      summary: Create or update adapter status
       description: |-
-        Adapter creates its initial status object for this cluster.
-        Returns 409 if adapter already has a status object for this cluster 
+        Adapter creates or updates its status report for this cluster.
+        If adapter already has a status, it will be updated (upsert by adapter name).
 
-        Response includes the status id and href for future operations.
-        Adapter should store the returned id/href to update its status later.
+        Response includes the full adapter status with all conditions.
+        Adapter should call this endpoint every time it evaluates the cluster.
       parameters:
         - name: cluster_id
           in: path
@@ -374,7 +374,7 @@ components:
       required: false
       schema:
         type: string
-        default: created_at
+        default: created_time
       explode: false
     QueryParams.page:
       name: page
@@ -412,29 +412,91 @@ components:
           description: labels for the API resource as pairs of name:value strings
       allOf:
         - $ref: '#/components/schemas/ObjectReference'
+    AdapterCondition:
+      type: object
+      allOf:
+        - $ref: '#/components/schemas/ConditionBase'
+      description: |-
+        Condition in AdapterStatus
+        Used for standard Kubernetes condition types: "Available", "Applied", "Health"
+        Note: observed_generation is at AdapterStatus level, not per-condition,
+        since all conditions in one AdapterStatus share the same observed generation
     AdapterStatus:
+      type: object
+      required:
+        - conditions
+        - created_time
+        - last_report_time
+      properties:
+        conditions:
+          type: array
+          items:
+            $ref: '#/components/schemas/AdapterCondition'
+          description: |-
+            Kubernetes-style conditions tracking adapter state
+            Typically includes: Available, Applied, Health
+        created_time:
+          type: string
+          format: date-time
+          description: When this adapter status was first created (API-managed)
+        last_report_time:
+          type: string
+          format: date-time
+          description: |-
+            When this adapter last reported its status (API-managed)
+            Updated every time the adapter POSTs, even if conditions haven't changed
+            Used by Sentinel to detect adapter liveness
+      allOf:
+        - $ref: '#/components/schemas/AdapterStatusBase'
+      description: |-
+        AdapterStatus represents the complete status report from an adapter
+        Contains multiple conditions, job metadata, and adapter-specific data
+      example:
+        adapter: validator
+        observed_generation: 1
+        conditions:
+          - type: Available
+            status: 'True'
+            reason: All validations passed
+            message: All 30 validation tests passed
+            last_transition_time: '2021-01-01T10:00:00Z'
+          - type: Applied
+            status: 'True'
+            reason: Validation job applied
+            message: Validation job applied successfully
+            last_transition_time: '2021-01-01T10:00:00Z'
+          - type: Health
+            status: 'True'
+            reason: All adapter operations completed successfully
+            message: All adapter runtime operations completed successfully
+            last_transition_time: '2021-01-01T10:00:00Z'
+        metadata:
+          job_name: validator-job-abc123
+          job_namespace: hyperfleet-system
+          attempt: 1
+          started_time: '2021-01-01T10:00:00Z'
+          completed_time: '2021-01-01T10:02:00Z'
+          duration: 2m
+        data:
+          validation_results:
+            total_tests: 30
+            passed: 30
+            failed: 0
+        created_time: '2021-01-01T10:00:00Z'
+        last_report_time: '2021-01-01T10:02:00Z'
+    AdapterStatusBase:
       type: object
       required:
         - adapter
         - observed_generation
-        - conditions
       properties:
         adapter:
           type: string
-          description: Name of the adapter that generated this status (Validator, DNS...)
+          description: Adapter name (e.g., "validator", "dns", "provisioner")
         observed_generation:
           type: integer
           format: int32
-          description: Which generation for an entity (Clusters, NodePools) was current at the time of creating this status
-        conditions:
-          type: array
-          items:
-            $ref: '#/components/schemas/Condition'
-          description: Kubernetes-style conditions tracking adapter state
-        data:
-          type: object
-          additionalProperties: {}
-          description: Adapter-specific data (structure varies by adapter type)
+          description: Which generation of the resource this status reflects
         metadata:
           type: object
           properties:
@@ -445,102 +507,128 @@ components:
             attempt:
               type: integer
               format: int32
-            started_at:
+            started_time:
               type: string
-            completed_at:
+              format: date-time
+            completed_time:
               type: string
+              format: date-time
             duration:
               type: string
-          description: Metadata about the adapter job/execution
+          description: Job execution metadata
+        data:
+          type: object
+          additionalProperties: {}
+          description: Adapter-specific data (structure varies by adapter type)
+      description: Base fields shared by AdapterStatus and AdapterStatusCreateRequest
+    AdapterStatusCreateRequest:
+      type: object
+      required:
+        - observed_time
+        - conditions
+      properties:
+        observed_time:
+          type: string
+          format: date-time
+          description: |-
+            When the adapter observed this resource state
+            API will use this to set AdapterStatus.last_report_time
+        conditions:
+          type: array
+          items:
+            $ref: '#/components/schemas/ConditionRequest'
+      allOf:
+        - $ref: '#/components/schemas/AdapterStatusBase'
+      description: Request payload for creating/updating adapter status
       example:
         adapter: validator
         observed_generation: 1
+        observed_time: '2021-01-01T10:00:00Z'
         conditions:
-          - adapter: validator
-            type: Available
+          - type: Available
             status: 'True'
             reason: All validations passed
             message: All 30 validation tests passed
-            observed_generation: 1
-            created_at: '2021-01-01T00:00:00Z'
-            updated_at: '2021-01-01T00:00:00Z'
-          - adapter: validator
-            type: Applied
+          - type: Applied
             status: 'True'
             reason: Validation job applied
             message: Validation job applied successfully
-            observed_generation: 1
-            created_at: '2021-01-01T00:00:00Z'
-            updated_at: '2021-01-01T00:00:00Z'
-          - adapter: validator
-            type: Health
+          - type: Health
             status: 'True'
-            reason: Validation job healthy
-            message: Validation job is healthy
-            observed_generation: 1
-            created_at: '2021-01-01T00:00:00Z'
-            updated_at: '2021-01-01T00:00:00Z'
-        data:
-          providerProperty1: providerValue1
-          providerProperty2: providerValue2
+            reason: All adapter operations completed successfully
+            message: All adapter runtime operations completed successfully
         metadata:
-          job_name: validator-job
-          job_namespace: default
+          job_name: validator-job-abc123
+          job_namespace: hyperfleet-system
           attempt: 1
-          started_at: '2021-01-01T00:00:00Z'
-          completed_at: '2021-01-01T00:00:00Z'
-          duration: 10s
-    AdapterStatusCreateRequest:
-      type: object
-      required:
-        - adapter
-        - observed_generation
-        - conditions
-      properties:
-        adapter:
-          type: string
-          description: Adapter identifier
-        observed_generation:
-          type: integer
-          format: int32
-          description: Which cluster generation this status reflects
-        conditions:
-          type: array
-          items:
-            $ref: '#/components/schemas/Condition'
+          started_time: '2021-01-01T10:00:00Z'
+          completed_time: '2021-01-01T10:02:00Z'
+          duration: 2m
         data:
-          type: object
-          additionalProperties: {}
-          description: Adapter-specific data
-        metadata:
-          type: object
-          additionalProperties: {}
-          description: Job execution metadata
+          validation_results:
+            total_tests: 30
+            passed: 30
+            failed: 0
     AdapterStatusList:
       type: object
       required:
+        - kind
         - items
       properties:
+        kind:
+          type: string
+          enum:
+            - AdapterStatusList
         items:
           type: array
           items:
             $ref: '#/components/schemas/AdapterStatus'
       allOf:
         - $ref: '#/components/schemas/List'
+      description: List of adapter statuses with pagination metadata
+      example:
+        kind: AdapterStatusList
+        page: 1
+        size: 2
+        total: 2
+        items:
+          - adapter: validator
+            observed_generation: 1
+            conditions:
+              - type: Available
+                status: 'True'
+                reason: All validations passed
+                message: All 30 validation tests passed
+                last_transition_time: '2021-01-01T10:00:00Z'
+            metadata:
+              job_name: validator-job-abc123
+              duration: 2m
+            created_time: '2021-01-01T10:00:00Z'
+            last_report_time: '2021-01-01T10:02:00Z'
+          - adapter: dns
+            observed_generation: 1
+            conditions:
+              - type: Available
+                status: 'True'
+                reason: DNS configured
+                message: DNS records created
+                last_transition_time: '2021-01-01T10:01:00Z'
+            created_time: '2021-01-01T10:01:00Z'
+            last_report_time: '2021-01-01T10:01:30Z'
     Cluster:
       type: object
       required:
-        - created_at
-        - updated_at
+        - created_time
+        - updated_time
         - created_by
         - updated_by
         - generation
         - status
       properties:
-        created_at:
+        created_time:
           type: string
           format: date-time
-        updated_at:
+        updated_time:
           type: string
           format: date-time
         created_by:
@@ -567,31 +655,31 @@ components:
           environment: production
           team: platform
         spec: {}
-        created_at: '2021-01-01T00:00:00Z'
-        updated_at: '2021-01-01T00:00:00Z'
+        created_time: '2021-01-01T00:00:00Z'
+        updated_time: '2021-01-01T00:00:00Z'
         generation: 1
         status:
           phase: Ready
           last_transition_time: '2021-01-01T00:00:00Z'
           observed_generation: 1
-          updated_at: '2021-01-01T00:00:00Z'
-          adapters:
-            - adapter: validator
-              type: Available
+          last_updated_time: '2021-01-01T00:00:00Z'
+          conditions:
+            - type: ValidationSuccessful
               status: 'True'
               reason: All validations passed
               message: All 30 validation tests passed
               observed_generation: 1
-              created_at: '2021-01-01T00:00:00Z'
-              updated_at: '2021-01-01T00:00:00Z'
-            - adapter: dns
-              type: Available
+              created_time: '2021-01-01T10:00:00Z'
+              last_updated_time: '2021-01-01T10:00:00Z'
+              last_transition_time: '2021-01-01T10:00:00Z'
+            - type: DNSSuccessful
               status: 'True'
-              reason: DNS records created
-              message: custom.domain.com created
+              reason: DNS configured
+              message: DNS records created for custom.domain.com
               observed_generation: 1
-              created_at: '2021-01-01T00:00:00Z'
-              updated_at: '2021-01-01T00:00:00Z'
+              created_time: '2021-01-01T10:01:00Z'
+              last_updated_time: '2021-01-01T10:01:00Z'
+              last_transition_time: '2021-01-01T10:01:00Z'
         created_by: user-123@example.com
         updated_by: user-123@example.com
     ClusterBase:
@@ -653,8 +741,8 @@ components:
         - phase
         - last_transition_time
         - observed_generation
-        - updated_at
-        - adapters
+        - last_updated_time
+        - conditions
       properties:
         phase:
           type: string
@@ -664,55 +752,51 @@ components:
             - Failed
           description: |-
             Current cluster phase (native database column).
-            Updated when adapters report status.
-            Note: status.phase provides aggregated view from all adapter conditions.
+            Updated when conditions are reported.
+            Note: status.phase provides aggregated view from all conditions.
         last_transition_time:
           type: string
           format: date-time
           description: |-
-            When cluster last transitioned (updated by adapters, used by Sentinel for backoff)
-            Updated when adapters report status if the phase changes
+            When cluster last transitioned (used by Sentinel for backoff)
+            Updated when conditions are reported if the phase changes
         observed_generation:
           type: integer
           format: int32
           description: |-
-            Last generation processed by adapters
-            Updated when adapters report status.
-            This will be the lowest value of each adapter's observed_generation values
+            Last generation processed
+            Updated when conditions are reported.
+            This will be the lowest value of each condition's observed_generation values
             The phase value is based on this generation
-        updated_at:
+        last_updated_time:
           type: string
           format: date-time
           description: |-
-            Time of the last complete report from adapters
-            Updated when adapters report status.
-            Oldest `updated_at` from the adapter conditions
-        adapters:
+            Time of the last update
+            Updated when conditions are reported.
+            Computed as min(conditions[].last_updated_time) to detect stale adapters.
+            Uses earliest (not latest) timestamp to ensure Sentinel can detect if any adapter has stopped reporting.
+        conditions:
           type: array
           items:
-            $ref: '#/components/schemas/ConditionAvailable'
+            $ref: '#/components/schemas/ResourceCondition'
       description: |-
-        Cluster status aggregation from all adapters.
+        Cluster status computed from all status conditions.
 
         This object is computed by the service and CANNOT be modified directly.
-        It is aggregated from adapter status updates posted to `/clusters/{id}/statuses`.
+        It is aggregated from condition updates posted to `/clusters/{id}/statuses`.
 
-        Provides quick overview of which adapters have reported and aggregated phase.
-    Condition:
+        Provides quick overview of all reported conditions and aggregated phase.
+    ConditionBase:
       type: object
       required:
-        - adapter
         - type
         - status
-        - observed_generation
-        - created_at
-        - updated_at
+        - last_transition_time
       properties:
-        adapter:
-          type: string
-          description: Adapter name
         type:
           type: string
+          description: Condition type
         status:
           type: string
           enum:
@@ -726,51 +810,19 @@ components:
         message:
           type: string
           description: Human-readable message
-        observed_generation:
-          type: integer
-          format: int32
-        created_at:
-          type: string
-          format: date-time
-          description: |-
-            Time when the condition was created
-            - In an adapter reporting conditions `updated_at==created_at`
-            - In the API, `created_at` doesn't change with new updates from adapters
-        updated_at:
-          type: string
-          format: date-time
-          description: Time when the condition was updated
         last_transition_time:
           type: string
           format: date-time
-          description: When this condition last transitioned (computed by service when status changes)
-    ConditionAvailable:
-      type: object
-      required:
-        - type
-      properties:
-        type:
-          type: string
-          enum:
-            - Available
-      allOf:
-        - $ref: '#/components/schemas/ConditionBase'
-      description: |-
-        StatusAvailable is the status condition from an adapter that is used by CLM to compute the phase.
-        `phase=ready` when all adaptors report `Available=true`
-    ConditionBase:
+          description: |-
+            When this condition last transitioned status (API-managed)
+            Only updated when status changes (True/False/Unknown), not when reason/message changes
+      description: Base condition fields shared by all condition types
+    ConditionRequest:
       type: object
       required:
-        - adapter
         - type
         - status
-        - observed_generation
-        - created_at
-        - updated_at
       properties:
-        adapter:
-          type: string
-          description: Adapter name
         type:
           type: string
         status:
@@ -779,31 +831,13 @@ components:
             - 'True'
             - 'False'
             - Unknown
-          description: Condition status
         reason:
           type: string
-          description: Machine-readable reason code
         message:
           type: string
-          description: Human-readable message
-        observed_generation:
-          type: integer
-          format: int32
-        created_at:
-          type: string
-          format: date-time
-          description: |-
-            Time when the condition was created
-            - In an adapter reporting conditions `updated_at==created_at`
-            - In the API, `created_at` doesn't change with new updates from adapters
-        updated_at:
-          type: string
-          format: date-time
-          description: Time when the condition was updated
       description: |-
-        Common data for status objects, part of:
-        - Status entity (stored in DB)
-        - Request payload
+        Condition data for create/update requests (from adapters)
+        observed_generation and observed_time are now at AdapterStatusCreateRequest level
     Error:
       type: object
       properties:
@@ -847,17 +881,17 @@ components:
     NodePool:
       type: object
       required:
-        - created_at
-        - updated_at
+        - created_time
+        - updated_time
         - created_by
         - updated_by
         - owner_references
         - status
       properties:
-        created_at:
+        created_time:
           type: string
           format: date-time
-        updated_at:
+        updated_time:
           type: string
           format: date-time
         created_by:
@@ -872,6 +906,45 @@ components:
           $ref: '#/components/schemas/NodePoolStatus'
       allOf:
         - $ref: '#/components/schemas/NodePoolBase'
+      example:
+        kind: NodePool
+        id: nodepool-123
+        href: https://api.hyperfleet.com/v1/nodepools/nodepool-123
+        name: worker-pool-1
+        labels:
+          environment: production
+          pooltype: worker
+        spec: {}
+        created_time: '2021-01-01T00:00:00Z'
+        updated_time: '2021-01-01T00:00:00Z'
+        created_by: user-123@example.com
+        updated_by: user-123@example.com
+        owner_references:
+          id: cluster-123
+          kind: Cluster
+          href: https://api.hyperfleet.com/v1/clusters/cluster-123
+        status:
+          phase: Ready
+          last_transition_time: '2021-01-01T10:00:00Z'
+          observed_generation: 1
+          last_updated_time: '2021-01-01T10:02:00Z'
+          conditions:
+            - type: ValidationSuccessful
+              status: 'True'
+              reason: All validations passed
+              message: NodePool validation passed
+              observed_generation: 1
+              created_time: '2021-01-01T10:00:00Z'
+              last_updated_time: '2021-01-01T10:00:00Z'
+              last_transition_time: '2021-01-01T10:00:00Z'
+            - type: NodePoolSuccessful
+              status: 'True'
+              reason: NodePool provisioned successfully
+              message: NodePool has been scaled to desired count
+              observed_generation: 1
+              created_time: '2021-01-01T10:01:00Z'
+              last_updated_time: '2021-01-01T10:01:00Z'
+              last_transition_time: '2021-01-01T10:01:00Z'
     NodePoolBase:
       type: object
       required:
@@ -932,11 +1005,17 @@ components:
             Cluster specification
             CLM doesn't know how to unmarshall the spec, it only stores and forwards to adapters to do their job
             But CLM will validate the schema before accepting the request
+      example:
+        name: worker-pool-1
+        labels:
+          environment: production
+          pooltype: worker
+        spec: {}
     NodePoolCreateResponse:
       type: object
       required:
-        - created_at
-        - updated_at
+        - created_time
+        - updated_time
         - created_by
         - updated_by
         - owner_references
@@ -944,10 +1023,10 @@ components:
         - name
         - spec
       properties:
-        created_at:
+        created_time:
           type: string
           format: date-time
-        updated_at:
+        updated_time:
           type: string
           format: date-time
         created_by:
@@ -1007,8 +1086,8 @@ components:
         - phase
         - observed_generation
         - last_transition_time
-        - updated_at
-        - adapters
+        - last_updated_time
+        - conditions
       properties:
         phase:
           type: string
@@ -1018,34 +1097,35 @@ components:
             - Failed
           description: |-
             Current NodePool phase (native database column).
-            Updated when adapters report status.
-            Note: status.phase provides aggregated view from all adapter conditions.
+            Updated when conditions are reported.
+            Note: status.phase provides aggregated view from all conditions.
         observed_generation:
           type: integer
           format: int32
           minimum: 1
           description: |-
-            Last generation processed by adapters
-            Updated when adapters report status.
-            This will be the lowest value of each adapter's observed_generation values
+            Last generation processed
+            Updated when conditions are reported.
+            This will be the lowest value of each condition's observed_generation values
             The phase value is based on this generation
         last_transition_time:
           type: string
           format: date-time
-          description: When NodePool last transitioned (updated by adapters, used by Sentinel for backoff)
-        updated_at:
+          description: When NodePool last transitioned (used by Sentinel for backoff)
+        last_updated_time:
           type: string
           format: date-time
           description: |-
-            Time of the last complete report from adapters
-            Updated when adapters report status.
-            Oldest `updated_at` from the adapter conditions
-        adapters:
+            Time of the last update
+            Updated when conditions are reported.
+            Computed as min(conditions[].last_updated_time) to detect stale adapters.
+            Uses earliest (not latest) timestamp to ensure Sentinel can detect if any adapter has stopped reporting.
+        conditions:
           type: array
           items:
-            $ref: '#/components/schemas/ConditionAvailable'
+            $ref: '#/components/schemas/ResourceCondition'
       description: |-
-        NodePool status aggregation from all adapters.
+        NodePool status computed from all status conditions.
 
         This object is computed by the service and CANNOT be modified directly.
     ObjectReference:
@@ -1065,6 +1145,34 @@ components:
       enum:
         - asc
         - desc
+    ResourceCondition:
+      type: object
+      required:
+        - observed_generation
+        - created_time
+        - last_updated_time
+      properties:
+        observed_generation:
+          type: integer
+          format: int32
+          description: Generation of the spec that this condition reflects
+        created_time:
+          type: string
+          format: date-time
+          description: When this condition was first created (API-managed)
+        last_updated_time:
+          type: string
+          format: date-time
+          description: |-
+            When the corresponding adapter last reported (API-managed)
+            Updated every time the adapter POSTs, even if condition status hasn't changed
+            Copied from AdapterStatus.last_report_time
+      allOf:
+        - $ref: '#/components/schemas/ConditionBase'
+      description: |-
+        Condition in Cluster/NodePool status
+        Used for semantic condition types: "ValidationSuccessful", "DNSSuccessful", "NodePoolSuccessful", etc.
+        Includes observed_generation and last_updated_time to track adapter-specific state
   securitySchemes:
     BearerAuth:
       type: http
diff --git a/pkg/api/adapter_status_types.go b/pkg/api/adapter_status_types.go
index 767a0113..bfe99eb7 100644
--- a/pkg/api/adapter_status_types.go
+++ b/pkg/api/adapter_status_types.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"encoding/json"
+	"time"
 
 	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi"
 	"gorm.io/datatypes"
@@ -10,7 +11,7 @@ import (
 
 // AdapterStatus database model
 type AdapterStatus struct {
-	Meta // Contains ID, CreatedAt, UpdatedAt, DeletedAt
+	Meta // Contains ID, CreatedTime, UpdatedTime, DeletedAt
 
 	// Polymorphic association
 	ResourceType string `json:"resource_type" gorm:"size:20;index:idx_resource;not null"`
@@ -20,6 +21,10 @@ type AdapterStatus struct {
 	Adapter            string `json:"adapter" gorm:"size:255;not null;uniqueIndex:idx_resource_adapter"`
 	ObservedGeneration int32  `json:"observed_generation" gorm:"not null"`
 
+	// API-managed timestamps
+	LastReportTime *time.Time `json:"last_report_time" gorm:"not null"` // Updated on every POST
+	CreatedTime    *time.Time `json:"created_time" gorm:"not null"`     // Set on first creation
+
 	// Stored as JSON
 	Conditions datatypes.JSON `json:"conditions" gorm:"type:jsonb;not null"`
 	Data       datatypes.JSON `json:"data,omitempty" gorm:"type:jsonb"`
@@ -45,29 +50,42 @@ func (as *AdapterStatus) BeforeCreate(tx *gorm.DB) error {
 // ToOpenAPI converts to OpenAPI model
 func (as *AdapterStatus) ToOpenAPI() *openapi.AdapterStatus {
 	// Unmarshal Conditions
-	var conditions []openapi.Condition
+	var conditions []openapi.AdapterCondition
 	if len(as.Conditions) > 0 {
 		_ = json.Unmarshal(as.Conditions, &conditions)
 	}
 
 	// Unmarshal Data
-	var data map[string]interface{}
+	var data map[string]map[string]interface{}
 	if len(as.Data) > 0 {
 		_ = json.Unmarshal(as.Data, &data)
 	}
 
 	// Unmarshal Metadata
-	var metadata *openapi.AdapterStatusMetadata
+	var metadata *openapi.AdapterStatusBaseMetadata
 	if len(as.Metadata) > 0 {
 		_ = json.Unmarshal(as.Metadata, &metadata)
 	}
 
+	// Set default times if nil (shouldn't happen in normal operation)
+	createdTime := time.Time{}
+	if as.CreatedTime != nil {
+		createdTime = *as.CreatedTime
+	}
+
+	lastReportTime := time.Time{}
+	if as.LastReportTime != nil {
+		lastReportTime = *as.LastReportTime
+	}
+
 	return &openapi.AdapterStatus{
 		Adapter:            as.Adapter,
 		ObservedGeneration: as.ObservedGeneration,
 		Conditions:         conditions,
 		Data:               data,
 		Metadata:           metadata,
+		CreatedTime:        createdTime,
+		LastReportTime:     lastReportTime,
 	}
 }
 
@@ -76,11 +94,30 @@ func AdapterStatusFromOpenAPICreate(
 	resourceType, resourceID string,
 	req *openapi.AdapterStatusCreateRequest,
 ) *AdapterStatus {
+	// Set timestamps
+	// CreatedTime and LastReportTime should be set from req.ObservedTime
+	now := time.Now()
+	if !req.ObservedTime.IsZero() {
+		now = req.ObservedTime
+	}
+
+	// Convert ConditionRequest to AdapterCondition (adding LastTransitionTime)
+	adapterConditions := make([]openapi.AdapterCondition, len(req.Conditions))
+	for i, condReq := range req.Conditions {
+		adapterConditions[i] = openapi.AdapterCondition{
+			Type:               condReq.Type,
+			Status:             condReq.Status,
+			Reason:             condReq.Reason,
+			Message:            condReq.Message,
+			LastTransitionTime: now,
+		}
+	}
+
 	// Marshal Conditions
-	conditionsJSON, _ := json.Marshal(req.Conditions)
+	conditionsJSON, _ := json.Marshal(adapterConditions)
 
 	// Marshal Data
-	data := make(map[string]interface{})
+	data := make(map[string]map[string]interface{})
 	if req.Data != nil {
 		data = req.Data
 	}
@@ -100,5 +137,7 @@ func AdapterStatusFromOpenAPICreate(
 		Conditions:         conditionsJSON,
 		Data:               dataJSON,
 		Metadata:           metadataJSON,
+		CreatedTime:        &now,
+		LastReportTime:     &now,
 	}
 }
diff --git a/pkg/api/cluster_types.go b/pkg/api/cluster_types.go
index d817813c..c383f0a9 100644
--- a/pkg/api/cluster_types.go
+++ b/pkg/api/cluster_types.go
@@ -27,8 +27,8 @@ type Cluster struct {
 	StatusPhase              string         `json:"status_phase" gorm:"default:'NotReady'"`
 	StatusLastTransitionTime *time.Time     `json:"status_last_transition_time,omitempty"`
 	StatusObservedGeneration int32          `json:"status_observed_generation" gorm:"default:0"`
-	StatusUpdatedAt          *time.Time     `json:"status_updated_at,omitempty"`
-	StatusAdapters           datatypes.JSON `json:"status_adapters" gorm:"type:jsonb"`
+	StatusLastUpdatedTime    *time.Time     `json:"status_last_updated_time,omitempty"`
+	StatusConditions         datatypes.JSON `json:"status_conditions" gorm:"type:jsonb"`
 
 	// Audit fields
 	CreatedBy string `json:"created_by" gorm:"size:255;not null"`
@@ -78,10 +78,10 @@ func (c *Cluster) ToOpenAPI() *openapi.Cluster {
 		_ = json.Unmarshal(c.Labels, &labels)
 	}
 
-	// Unmarshal StatusAdapters
-	var statusAdapters []openapi.ConditionAvailable
-	if len(c.StatusAdapters) > 0 {
-		_ = json.Unmarshal(c.StatusAdapters, &statusAdapters)
+	// Unmarshal StatusConditions
+	var statusConditions []openapi.ResourceCondition
+	if len(c.StatusConditions) > 0 {
+		_ = json.Unmarshal(c.StatusConditions, &statusConditions)
 	}
 
 	// Generate Href if not set (fallback)
@@ -91,32 +91,36 @@ func (c *Cluster) ToOpenAPI() *openapi.Cluster {
 	}
 
 	cluster := &openapi.Cluster{
-		Id:         &c.ID,
-		Kind:       c.Kind,
-		Href:       &href,
-		Name:       c.Name,
-		Spec:       spec,
-		Labels:     &labels,
-		Generation: c.Generation,
-		CreatedAt:  c.CreatedAt,
-		UpdatedAt:  c.UpdatedAt,
-		CreatedBy:  c.CreatedBy,
-		UpdatedBy:  c.UpdatedBy,
+		Id:          &c.ID,
+		Kind:        c.Kind,
+		Href:        &href,
+		Name:        c.Name,
+		Spec:        spec,
+		Labels:      &labels,
+		Generation:  c.Generation,
+		CreatedTime: c.CreatedTime,
+		UpdatedTime: c.UpdatedTime,
+		CreatedBy:   c.CreatedBy,
+		UpdatedBy:   c.UpdatedBy,
 	}
 
-	// Build ClusterStatus
-	cluster.Status = openapi.ClusterStatus{
-		Phase:              c.StatusPhase,
-		ObservedGeneration: c.StatusObservedGeneration,
-		Adapters:           statusAdapters,
+	// Build ClusterStatus - set required fields with defaults if nil
+	lastTransitionTime := time.Time{}
+	if c.StatusLastTransitionTime != nil {
+		lastTransitionTime = *c.StatusLastTransitionTime
 	}
 
-	if c.StatusLastTransitionTime != nil {
-		cluster.Status.LastTransitionTime = *c.StatusLastTransitionTime
+	lastUpdatedTime := time.Time{}
+	if c.StatusLastUpdatedTime != nil {
+		lastUpdatedTime = *c.StatusLastUpdatedTime
 	}
 
-	if c.StatusUpdatedAt != nil {
-		cluster.Status.UpdatedAt = *c.StatusUpdatedAt
+	cluster.Status = openapi.ClusterStatus{
+		Phase:              c.StatusPhase,
+		ObservedGeneration: c.StatusObservedGeneration,
+		Conditions:         statusConditions,
+		LastTransitionTime: lastTransitionTime,
+		LastUpdatedTime:    lastUpdatedTime,
 	}
 
 	return cluster
@@ -134,8 +138,8 @@ func ClusterFromOpenAPICreate(req *openapi.ClusterCreateRequest, createdBy strin
 	}
 	labelsJSON, _ := json.Marshal(labels)
 
-	// Marshal empty StatusAdapters
-	statusAdaptersJSON, _ := json.Marshal([]openapi.ConditionAvailable{})
+	// Marshal empty StatusConditions
+	statusConditionsJSON, _ := json.Marshal([]openapi.ResourceCondition{})
 
 	return &Cluster{
 		Kind:                     req.Kind,
@@ -145,7 +149,7 @@ func ClusterFromOpenAPICreate(req *openapi.ClusterCreateRequest, createdBy strin
 		Generation:               1,
 		StatusPhase:              "NotReady",
 		StatusObservedGeneration: 0,
-		StatusAdapters:           statusAdaptersJSON,
+		StatusConditions:         statusConditionsJSON,
 		CreatedBy:                createdBy,
 		UpdatedBy:                createdBy,
 	}
diff --git a/pkg/api/metadata_types.go b/pkg/api/metadata_types.go
index 24b89d4c..8c70e37c 100755
--- a/pkg/api/metadata_types.go
+++ b/pkg/api/metadata_types.go
@@ -35,10 +35,10 @@ type Metadata struct {
 
 // Meta is base model definition, embedded in all kinds
 type Meta struct {
-	ID        string
-	CreatedAt time.Time
-	UpdatedAt time.Time
-	DeletedAt gorm.DeletedAt `gorm:"index"`
+	ID          string
+	CreatedTime time.Time
+	UpdatedTime time.Time
+	DeletedAt   gorm.DeletedAt `gorm:"index"`
 }
 
 // PagingMeta List Paging metadata
diff --git a/pkg/api/node_pool_types.go b/pkg/api/node_pool_types.go
index cabfa595..5955f2e0 100644
--- a/pkg/api/node_pool_types.go
+++ b/pkg/api/node_pool_types.go
@@ -33,8 +33,8 @@ type NodePool struct {
 	StatusPhase              string         `json:"status_phase" gorm:"default:'NotReady'"`
 	StatusObservedGeneration int32          `json:"status_observed_generation" gorm:"default:0"`
 	StatusLastTransitionTime *time.Time     `json:"status_last_transition_time,omitempty"`
-	StatusUpdatedAt          *time.Time     `json:"status_updated_at,omitempty"`
-	StatusAdapters           datatypes.JSON `json:"status_adapters" gorm:"type:jsonb"`
+	StatusLastUpdatedTime    *time.Time     `json:"status_last_updated_time,omitempty"`
+	StatusConditions         datatypes.JSON `json:"status_conditions" gorm:"type:jsonb"`
 
 	// Audit fields
 	CreatedBy string `json:"created_by" gorm:"size:255;not null"`
@@ -88,10 +88,10 @@ func (np *NodePool) ToOpenAPI() *openapi.NodePool {
 		_ = json.Unmarshal(np.Labels, &labels)
 	}
 
-	// Unmarshal StatusAdapters
-	var statusAdapters []openapi.ConditionAvailable
-	if len(np.StatusAdapters) > 0 {
-		_ = json.Unmarshal(np.StatusAdapters, &statusAdapters)
+	// Unmarshal StatusConditions
+	var statusConditions []openapi.ResourceCondition
+	if len(np.StatusConditions) > 0 {
+		_ = json.Unmarshal(np.StatusConditions, &statusConditions)
 	}
 
 	// Generate Href if not set (fallback)
@@ -119,25 +119,29 @@ func (np *NodePool) ToOpenAPI() *openapi.NodePool {
 			Kind: &np.OwnerKind,
 			Href: &ownerHref,
 		},
-		CreatedAt: np.CreatedAt,
-		UpdatedAt: np.UpdatedAt,
-		CreatedBy: np.CreatedBy,
-		UpdatedBy: np.UpdatedBy,
+		CreatedTime: np.CreatedTime,
+		UpdatedTime: np.UpdatedTime,
+		CreatedBy:   np.CreatedBy,
+		UpdatedBy:   np.UpdatedBy,
 	}
 
-	// Build NodePoolStatus
-	nodePool.Status = openapi.NodePoolStatus{
-		Phase:              np.StatusPhase,
-		ObservedGeneration: np.StatusObservedGeneration,
-		Adapters:           statusAdapters,
+	// Build NodePoolStatus - set required fields with defaults if nil
+	lastTransitionTime := time.Time{}
+	if np.StatusLastTransitionTime != nil {
+		lastTransitionTime = *np.StatusLastTransitionTime
 	}
 
-	if np.StatusLastTransitionTime != nil {
-		nodePool.Status.LastTransitionTime = *np.StatusLastTransitionTime
+	lastUpdatedTime := time.Time{}
+	if np.StatusLastUpdatedTime != nil {
+		lastUpdatedTime = *np.StatusLastUpdatedTime
 	}
 
-	if np.StatusUpdatedAt != nil {
-		nodePool.Status.UpdatedAt = *np.StatusUpdatedAt
+	nodePool.Status = openapi.NodePoolStatus{
+		Phase:              np.StatusPhase,
+		ObservedGeneration: np.StatusObservedGeneration,
+		Conditions:         statusConditions,
+		LastTransitionTime: lastTransitionTime,
+		LastUpdatedTime:    lastUpdatedTime,
 	}
 
 	return nodePool
@@ -155,8 +159,8 @@ func NodePoolFromOpenAPICreate(req *openapi.NodePoolCreateRequest, ownerID, crea
 	}
 	labelsJSON, _ := json.Marshal(labels)
 
-	// Marshal empty StatusAdapters
-	statusAdaptersJSON, _ := json.Marshal([]openapi.ConditionAvailable{})
+	// Marshal empty StatusConditions
+	statusConditionsJSON, _ := json.Marshal([]openapi.ResourceCondition{})
 
 	kind := "NodePool"
 	if req.Kind != nil {
@@ -172,7 +176,7 @@ func NodePoolFromOpenAPICreate(req *openapi.NodePoolCreateRequest, ownerID, crea
 		OwnerKind:                "Cluster",
 		StatusPhase:              "NotReady",
 		StatusObservedGeneration: 0,
-		StatusAdapters:           statusAdaptersJSON,
+		StatusConditions:         statusConditionsJSON,
 		CreatedBy:                createdBy,
 		UpdatedBy:                createdBy,
 	}
diff --git a/pkg/dao/adapter_status.go b/pkg/dao/adapter_status.go
index 30f07f81..c0f8c476 100644
--- a/pkg/dao/adapter_status.go
+++ b/pkg/dao/adapter_status.go
@@ -2,10 +2,16 @@ package dao
 
 import (
 	"context"
+	"encoding/json"
+	"errors"
+	"time"
 
+	"gorm.io/datatypes"
+	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 
 	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
+	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi"
 	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/db"
 )
 
@@ -13,6 +19,7 @@ type AdapterStatusDao interface {
 	Get(ctx context.Context, id string) (*api.AdapterStatus, error)
 	Create(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, error)
 	Replace(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, error)
+	Upsert(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, error)
 	Delete(ctx context.Context, id string) error
 	FindByResource(ctx context.Context, resourceType, resourceID string) (api.AdapterStatusList, error)
 	FindByResourcePaginated(ctx context.Context, resourceType, resourceID string, offset, limit int) (api.AdapterStatusList, int64, error)
@@ -57,6 +64,47 @@ func (d *sqlAdapterStatusDao) Replace(ctx context.Context, adapterStatus *api.Ad
 	return adapterStatus, nil
 }
 
+// Upsert creates or updates an adapter status based on resource_type, resource_id, and adapter
+// This implements the upsert semantic required by the new API spec
+func (d *sqlAdapterStatusDao) Upsert(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, error) {
+	g2 := (*d.sessionFactory).New(ctx)
+
+	// Try to find existing adapter status
+	existing, err := d.FindByResourceAndAdapter(ctx, adapterStatus.ResourceType, adapterStatus.ResourceID, adapterStatus.Adapter)
+
+	if err != nil {
+		// If not found, create new
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return d.Create(ctx, adapterStatus)
+		}
+		// Other errors
+		db.MarkForRollback(ctx, err)
+		return nil, err
+	}
+
+	// Update existing record
+	// Keep the original ID and CreatedTime
+	adapterStatus.ID = existing.ID
+	if existing.CreatedTime != nil {
+		adapterStatus.CreatedTime = existing.CreatedTime
+	}
+
+	// Update LastReportTime to now
+	now := time.Now()
+	adapterStatus.LastReportTime = &now
+
+	// Preserve LastTransitionTime for conditions whose status hasn't changed
+	adapterStatus.Conditions = preserveLastTransitionTime(existing.Conditions, adapterStatus.Conditions)
+
+	// Save (update) the record
+	if err := g2.Omit(clause.Associations).Save(adapterStatus).Error; err != nil {
+		db.MarkForRollback(ctx, err)
+		return nil, err
+	}
+
+	return adapterStatus, nil
+}
+
 func (d *sqlAdapterStatusDao) Delete(ctx context.Context, id string) error {
 	g2 := (*d.sessionFactory).New(ctx)
 	if err := g2.Omit(clause.Associations).Delete(&api.AdapterStatus{Meta: api.Meta{ID: id}}).Error; err != nil {
@@ -113,3 +161,52 @@ func (d *sqlAdapterStatusDao) All(ctx context.Context) (api.AdapterStatusList, e
 	}
 	return statuses, nil
 }
+
+// preserveLastTransitionTime preserves LastTransitionTime for conditions whose status hasn't changed
+// This implements the Kubernetes condition semantic where LastTransitionTime is only updated when status changes
+func preserveLastTransitionTime(oldConditionsJSON, newConditionsJSON datatypes.JSON) datatypes.JSON {
+	// Unmarshal old conditions
+	var oldConditions []openapi.AdapterCondition
+	if len(oldConditionsJSON) > 0 {
+		if err := json.Unmarshal(oldConditionsJSON, &oldConditions); err != nil {
+			// If we can't unmarshal old conditions, return new conditions as-is
+			return newConditionsJSON
+		}
+	}
+
+	// Unmarshal new conditions
+	var newConditions []openapi.AdapterCondition
+	if len(newConditionsJSON) > 0 {
+		if err := json.Unmarshal(newConditionsJSON, &newConditions); err != nil {
+			// If we can't unmarshal new conditions, return new conditions as-is
+			return newConditionsJSON
+		}
+	}
+
+	// Build a map of old conditions by type for quick lookup
+	oldConditionsMap := make(map[string]openapi.AdapterCondition)
+	for _, oldCond := range oldConditions {
+		oldConditionsMap[oldCond.Type] = oldCond
+	}
+
+	// Update new conditions: preserve LastTransitionTime if status hasn't changed
+	for i := range newConditions {
+		if oldCond, exists := oldConditionsMap[newConditions[i].Type]; exists {
+			// If status hasn't changed, preserve the old LastTransitionTime
+			if oldCond.Status == newConditions[i].Status {
+				newConditions[i].LastTransitionTime = oldCond.LastTransitionTime
+			}
+			// If status changed, keep the new LastTransitionTime (already set to now)
+		}
+		// If this is a new condition type, keep the new LastTransitionTime
+	}
+
+	// Marshal back to JSON
+	updatedJSON, err := json.Marshal(newConditions)
+	if err != nil {
+		// If we can't marshal, return new conditions as-is
+		return newConditionsJSON
+	}
+
+	return updatedJSON
+}
diff --git a/pkg/dao/cluster.go b/pkg/dao/cluster.go
index ecbb4dd0..6c077df0 100644
--- a/pkg/dao/cluster.go
+++ b/pkg/dao/cluster.go
@@ -1,6 +1,7 @@
 package dao
 
 import (
+	"bytes"
 	"context"
 
 	"gorm.io/gorm/clause"
@@ -48,6 +49,23 @@ func (d *sqlClusterDao) Create(ctx context.Context, cluster *api.Cluster) (*api.
 
 func (d *sqlClusterDao) Replace(ctx context.Context, cluster *api.Cluster) (*api.Cluster, error) {
 	g2 := (*d.sessionFactory).New(ctx)
+
+	// Get the existing cluster to compare spec
+	existing, err := d.Get(ctx, cluster.ID)
+	if err != nil {
+		db.MarkForRollback(ctx, err)
+		return nil, err
+	}
+
+	// Compare spec: if changed, increment generation
+	if !bytes.Equal(existing.Spec, cluster.Spec) {
+		cluster.Generation = existing.Generation + 1
+	} else {
+		// Spec unchanged, preserve generation
+		cluster.Generation = existing.Generation
+	}
+
+	// Save the cluster
 	if err := g2.Omit(clause.Associations).Save(cluster).Error; err != nil {
 		db.MarkForRollback(ctx, err)
 		return nil, err
diff --git a/pkg/db/migrations/202511111044_add_clusters.go b/pkg/db/migrations/202511111044_add_clusters.go
index f093ccc4..31455341 100644
--- a/pkg/db/migrations/202511111044_add_clusters.go
+++ b/pkg/db/migrations/202511111044_add_clusters.go
@@ -16,13 +16,13 @@ func addClusters() *gormigrate.Migration {
 		ID: "202511111044",
 		Migrate: func(tx *gorm.DB) error {
 			// Create clusters table
-			// ClusterStatus is stored as JSONB in status_adapters, and status fields
+			// ClusterStatus is stored as JSONB in status_conditions, and status fields
 			// are flattened for efficient querying
 			createTableSQL := `
 				CREATE TABLE IF NOT EXISTS clusters (
 					id VARCHAR(255) PRIMARY KEY,
-					created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-					updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
 					deleted_at TIMESTAMPTZ NULL,
 
 					-- Core fields
@@ -39,8 +39,8 @@ func addClusters() *gormigrate.Migration {
 					status_phase VARCHAR(50) NOT NULL DEFAULT 'NotReady',
 					status_last_transition_time TIMESTAMPTZ NULL,
 					status_observed_generation INTEGER NOT NULL DEFAULT 0,
-					status_updated_at TIMESTAMPTZ NULL,
-					status_adapters JSONB NULL,
+					status_last_updated_time TIMESTAMPTZ NULL,
+					status_conditions JSONB NULL,
 
 					-- Audit fields
 					created_by VARCHAR(255) NOT NULL,
diff --git a/pkg/db/migrations/202511111055_add_node_pools.go b/pkg/db/migrations/202511111055_add_node_pools.go
index b06b0960..d40411e2 100644
--- a/pkg/db/migrations/202511111055_add_node_pools.go
+++ b/pkg/db/migrations/202511111055_add_node_pools.go
@@ -19,8 +19,8 @@ func addNodePools() *gormigrate.Migration {
 			createTableSQL := `
 				CREATE TABLE IF NOT EXISTS node_pools (
 					id VARCHAR(255) PRIMARY KEY,
-					created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-					updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
 					deleted_at TIMESTAMPTZ NULL,
 
 					-- Core fields
@@ -42,8 +42,8 @@ func addNodePools() *gormigrate.Migration {
 					status_phase VARCHAR(50) NOT NULL DEFAULT 'NotReady',
 					status_last_transition_time TIMESTAMPTZ NULL,
 					status_observed_generation INTEGER NOT NULL DEFAULT 0,
-					status_updated_at TIMESTAMPTZ NULL,
-					status_adapters JSONB NULL,
+					status_last_updated_time TIMESTAMPTZ NULL,
+					status_conditions JSONB NULL,
 
 					-- Audit fields
 					created_by VARCHAR(255) NOT NULL,
diff --git a/pkg/db/migrations/202511111105_add_adapter_status.go b/pkg/db/migrations/202511111105_add_adapter_status.go
index 9bb21b93..947ba8c7 100644
--- a/pkg/db/migrations/202511111105_add_adapter_status.go
+++ b/pkg/db/migrations/202511111105_add_adapter_status.go
@@ -20,8 +20,8 @@ func addAdapterStatus() *gormigrate.Migration {
 			createTableSQL := `
 				CREATE TABLE IF NOT EXISTS adapter_statuses (
 					id VARCHAR(255) PRIMARY KEY,
-					created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-					updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+					updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
 					deleted_at TIMESTAMPTZ NULL,
 
 					-- Polymorphic association
@@ -32,6 +32,9 @@ func addAdapterStatus() *gormigrate.Migration {
 					adapter VARCHAR(255) NOT NULL,
 					observed_generation INTEGER NOT NULL,
 
+					-- API-managed timestamps
+					last_report_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
 					-- Stored as JSONB
 					conditions JSONB NOT NULL,
 					data JSONB NULL,
diff --git a/pkg/handlers/cluster_nodepools_test.go b/pkg/handlers/cluster_nodepools_test.go
index 625aa388..84a4c752 100644
--- a/pkg/handlers/cluster_nodepools_test.go
+++ b/pkg/handlers/cluster_nodepools_test.go
@@ -151,9 +151,9 @@ func TestClusterNodePoolsHandler_Get(t *testing.T) {
 			mockClusterFunc: func(ctx context.Context, id string) (*api.Cluster, *errors.ServiceError) {
 				return &api.Cluster{
 					Meta: api.Meta{
-						ID:        clusterID,
-						CreatedAt: now,
-						UpdatedAt: now,
+						ID:          clusterID,
+						CreatedTime: now,
+						UpdatedTime: now,
 					},
 					Name: "test-cluster",
 				}, nil
@@ -161,9 +161,9 @@ func TestClusterNodePoolsHandler_Get(t *testing.T) {
 			mockNodePoolFunc: func(ctx context.Context, id string) (*api.NodePool, *errors.ServiceError) {
 				return &api.NodePool{
 					Meta: api.Meta{
-						ID:        nodePoolID,
-						CreatedAt: now,
-						UpdatedAt: now,
+						ID:          nodePoolID,
+						CreatedTime: now,
+						UpdatedTime: now,
 					},
 					Kind:    "NodePool",
 					Name:    "test-nodepool",
@@ -193,9 +193,9 @@ func TestClusterNodePoolsHandler_Get(t *testing.T) {
 			mockClusterFunc: func(ctx context.Context, id string) (*api.Cluster, *errors.ServiceError) {
 				return &api.Cluster{
 					Meta: api.Meta{
-						ID:        clusterID,
-						CreatedAt: now,
-						UpdatedAt: now,
+						ID:          clusterID,
+						CreatedTime: now,
+						UpdatedTime: now,
 					},
 					Name: "test-cluster",
 				}, nil
@@ -213,9 +213,9 @@ func TestClusterNodePoolsHandler_Get(t *testing.T) {
 			mockClusterFunc: func(ctx context.Context, id string) (*api.Cluster, *errors.ServiceError) {
 				return &api.Cluster{
 					Meta: api.Meta{
-						ID:        clusterID,
-						CreatedAt: now,
-						UpdatedAt: now,
+						ID:          clusterID,
+						CreatedTime: now,
+						UpdatedTime: now,
 					},
 					Name: "test-cluster",
 				}, nil
@@ -223,9 +223,9 @@ func TestClusterNodePoolsHandler_Get(t *testing.T) {
 			mockNodePoolFunc: func(ctx context.Context, id string) (*api.NodePool, *errors.ServiceError) {
 				return &api.NodePool{
 					Meta: api.Meta{
-						ID:        nodePoolID,
-						CreatedAt: now,
-						UpdatedAt: now,
+						ID:          nodePoolID,
+						CreatedTime: now,
+						UpdatedTime: now,
 					},
 					Kind:    "NodePool",
 					Name:    "test-nodepool",
diff --git a/pkg/handlers/cluster_status.go b/pkg/handlers/cluster_status.go
index b45eb49d..aabb16fa 100644
--- a/pkg/handlers/cluster_status.go
+++ b/pkg/handlers/cluster_status.go
@@ -1,7 +1,6 @@
 package handlers
 
 import (
-	"encoding/json"
 	"net/http"
 
 	"github.com/gorilla/mux"
@@ -79,26 +78,11 @@ func (h clusterStatusHandler) Create(w http.ResponseWriter, r *http.Request) {
 				return nil, err
 			}
 
-			// Check if adapter status already exists
-			existing, _ := h.adapterStatusService.FindByResourceAndAdapter(ctx, "Cluster", clusterID, req.Adapter)
-
-			var adapterStatus *api.AdapterStatus
-			if existing != nil {
-				// Update existing
-				existing.ObservedGeneration = req.ObservedGeneration
-				conditionsJSON, _ := json.Marshal(req.Conditions)
-				existing.Conditions = conditionsJSON
-				if req.Data != nil {
-					dataJSON, _ := json.Marshal(req.Data)
-					existing.Data = dataJSON
-				}
-				adapterStatus, err = h.adapterStatusService.Replace(ctx, existing)
-			} else {
-				// Create new
-				newStatus := api.AdapterStatusFromOpenAPICreate("Cluster", clusterID, &req)
-				adapterStatus, err = h.adapterStatusService.Create(ctx, newStatus)
-			}
+			// Create adapter status from request
+			newStatus := api.AdapterStatusFromOpenAPICreate("Cluster", clusterID, &req)
 
+			// Upsert (create or update based on resource_type + resource_id + adapter)
+			adapterStatus, err := h.adapterStatusService.Upsert(ctx, newStatus)
 			if err != nil {
 				return nil, err
 			}
diff --git a/pkg/handlers/nodepool_status.go b/pkg/handlers/nodepool_status.go
index 582770f4..9de8aca2 100644
--- a/pkg/handlers/nodepool_status.go
+++ b/pkg/handlers/nodepool_status.go
@@ -1,7 +1,6 @@
 package handlers
 
 import (
-	"encoding/json"
 	"net/http"
 
 	"github.com/gorilla/mux"
@@ -79,26 +78,11 @@ func (h nodePoolStatusHandler) Create(w http.ResponseWriter, r *http.Request) {
 				return nil, err
 			}
 
-			// Check if adapter status already exists
-			existing, _ := h.adapterStatusService.FindByResourceAndAdapter(ctx, "NodePool", nodePoolID, req.Adapter)
-
-			var adapterStatus *api.AdapterStatus
-			if existing != nil {
-				// Update existing
-				existing.ObservedGeneration = req.ObservedGeneration
-				conditionsJSON, _ := json.Marshal(req.Conditions)
-				existing.Conditions = conditionsJSON
-				if req.Data != nil {
-					dataJSON, _ := json.Marshal(req.Data)
-					existing.Data = dataJSON
-				}
-				adapterStatus, err = h.adapterStatusService.Replace(ctx, existing)
-			} else {
-				// Create new
-				newStatus := api.AdapterStatusFromOpenAPICreate("NodePool", nodePoolID, &req)
-				adapterStatus, err = h.adapterStatusService.Create(ctx, newStatus)
-			}
+			// Create adapter status from request
+			newStatus := api.AdapterStatusFromOpenAPICreate("NodePool", nodePoolID, &req)
 
+			// Upsert (create or update based on resource_type + resource_id + adapter)
+			adapterStatus, err := h.adapterStatusService.Upsert(ctx, newStatus)
 			if err != nil {
 				return nil, err
 			}
diff --git a/pkg/services/adapter_status.go b/pkg/services/adapter_status.go
index 1295ffa7..7b564463 100644
--- a/pkg/services/adapter_status.go
+++ b/pkg/services/adapter_status.go
@@ -12,6 +12,7 @@ type AdapterStatusService interface {
 	Get(ctx context.Context, id string) (*api.AdapterStatus, *errors.ServiceError)
 	Create(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, *errors.ServiceError)
 	Replace(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, *errors.ServiceError)
+	Upsert(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, *errors.ServiceError)
 	Delete(ctx context.Context, id string) *errors.ServiceError
 	FindByResource(ctx context.Context, resourceType, resourceID string) (api.AdapterStatusList, *errors.ServiceError)
 	FindByResourcePaginated(ctx context.Context, resourceType, resourceID string, listArgs *ListArguments) (api.AdapterStatusList, int64, *errors.ServiceError)
@@ -55,6 +56,14 @@ func (s *sqlAdapterStatusService) Replace(ctx context.Context, adapterStatus *ap
 	return adapterStatus, nil
 }
 
+func (s *sqlAdapterStatusService) Upsert(ctx context.Context, adapterStatus *api.AdapterStatus) (*api.AdapterStatus, *errors.ServiceError) {
+	adapterStatus, err := s.adapterStatusDao.Upsert(ctx, adapterStatus)
+	if err != nil {
+		return nil, handleCreateError("AdapterStatus", err)
+	}
+	return adapterStatus, nil
+}
+
 func (s *sqlAdapterStatusService) Delete(ctx context.Context, id string) *errors.ServiceError {
 	if err := s.adapterStatusDao.Delete(ctx, id); err != nil {
 		return handleDeleteError("AdapterStatus", errors.GeneralError("Unable to delete adapter status: %s", err))
diff --git a/pkg/services/cluster.go b/pkg/services/cluster.go
index 466fdc69..4a8371ff 100644
--- a/pkg/services/cluster.go
+++ b/pkg/services/cluster.go
@@ -129,21 +129,19 @@ func (s *sqlClusterService) UpdateClusterStatusFromAdapters(ctx context.Context,
 		return nil, errors.GeneralError("Failed to get adapter statuses: %s", err)
 	}
 
-	// Build the list of ConditionAvailable
-	adapters := []openapi.ConditionAvailable{}
-	allReady := true
-	anyFailed := false
+	// Build the list of ResourceCondition
+	adapters := []openapi.ResourceCondition{}
 	maxObservedGeneration := int32(0)
 
 	for _, adapterStatus := range adapterStatuses {
 		// Unmarshal Conditions from JSONB
-		var conditions []openapi.Condition
+		var conditions []openapi.AdapterCondition
 		if err := json.Unmarshal(adapterStatus.Conditions, &conditions); err != nil {
 			continue // Skip if can't unmarshal
 		}
 
 		// Find the "Available" condition
-		var availableCondition *openapi.Condition
+		var availableCondition *openapi.AdapterCondition
 		for i := range conditions {
 			if conditions[i].Type == "Available" {
 				availableCondition = &conditions[i]
@@ -152,61 +150,77 @@ func (s *sqlClusterService) UpdateClusterStatusFromAdapters(ctx context.Context,
 		}
 
 		if availableCondition == nil {
-			// No Available condition means adapter is not ready
-			allReady = false
+			// No Available condition, skip this adapter
 			continue
 		}
 
-		// Convert to ConditionAvailable
-		condAvail := openapi.ConditionAvailable{
-			Type:               availableCondition.Type,
-			Adapter:            adapterStatus.Adapter,
+		// Convert to ResourceCondition
+		condResource := openapi.ResourceCondition{
+			Type:               MapAdapterToConditionType(adapterStatus.Adapter),
 			Status:             availableCondition.Status,
 			Reason:             availableCondition.Reason,
 			Message:            availableCondition.Message,
-			ObservedGeneration: availableCondition.ObservedGeneration,
+			ObservedGeneration: adapterStatus.ObservedGeneration,
+			LastTransitionTime: availableCondition.LastTransitionTime,
 		}
-		adapters = append(adapters, condAvail)
 
-		// Check status
-		if availableCondition.Status != "True" {
-			allReady = false
-			if availableCondition.Status == "False" {
-				anyFailed = true
-			}
+		// Set CreatedTime with nil check
+		if adapterStatus.CreatedTime != nil {
+			condResource.CreatedTime = *adapterStatus.CreatedTime
+		}
+
+		// Set LastUpdatedTime with nil check
+		if adapterStatus.LastReportTime != nil {
+			condResource.LastUpdatedTime = *adapterStatus.LastReportTime
 		}
 
+		adapters = append(adapters, condResource)
+
 		// Track max observed generation
 		if adapterStatus.ObservedGeneration > maxObservedGeneration {
 			maxObservedGeneration = adapterStatus.ObservedGeneration
 		}
 	}
 
-	// Compute overall phase
-	phase := "NotReady"
-	if len(adapterStatuses) > 0 {
-		if allReady {
-			phase = "Ready"
-		} else if anyFailed {
-			phase = "Failed"
+	// Compute overall phase using required adapters
+	newPhase := ComputePhase(ctx, adapterStatuses, requiredClusterAdapters, cluster.Generation)
+
+	// Calculate min(adapters[].last_report_time) for cluster.status.last_updated_time
+	// This uses the OLDEST adapter timestamp to ensure Sentinel can detect stale adapters
+	var minLastUpdatedTime *time.Time
+	for _, adapterStatus := range adapterStatuses {
+		if adapterStatus.LastReportTime != nil {
+			if minLastUpdatedTime == nil || adapterStatus.LastReportTime.Before(*minLastUpdatedTime) {
+				minLastUpdatedTime = adapterStatus.LastReportTime
+			}
 		}
 	}
 
+	// Save old phase to detect transitions
+	oldPhase := cluster.StatusPhase
+
 	// Update cluster status fields
 	now := time.Now()
-	cluster.StatusPhase = phase
+	cluster.StatusPhase = newPhase
 	cluster.StatusObservedGeneration = maxObservedGeneration
 
-	// Marshal adapters to JSON
-	adaptersJSON, err := json.Marshal(adapters)
+	// Marshal conditions to JSON
+	conditionsJSON, err := json.Marshal(adapters)
 	if err != nil {
-		return nil, errors.GeneralError("Failed to marshal adapters: %s", err)
+		return nil, errors.GeneralError("Failed to marshal conditions: %s", err)
+	}
+	cluster.StatusConditions = conditionsJSON
+
+	// Use min(adapters[].last_report_time) instead of now()
+	// This ensures Sentinel triggers reconciliation when ANY adapter is stale
+	if minLastUpdatedTime != nil {
+		cluster.StatusLastUpdatedTime = minLastUpdatedTime
+	} else {
+		cluster.StatusLastUpdatedTime = &now
 	}
-	cluster.StatusAdapters = adaptersJSON
-	cluster.StatusUpdatedAt = &now
 
-	// Update last transition time if phase changed
-	if cluster.StatusLastTransitionTime == nil || cluster.StatusPhase != phase {
+	// Update last transition time only if phase changed
+	if cluster.StatusLastTransitionTime == nil || oldPhase != newPhase {
 		cluster.StatusLastTransitionTime = &now
 	}
 
diff --git a/pkg/services/node_pool.go b/pkg/services/node_pool.go
index 63ee3303..9fa13363 100644
--- a/pkg/services/node_pool.go
+++ b/pkg/services/node_pool.go
@@ -129,21 +129,19 @@ func (s *sqlNodePoolService) UpdateNodePoolStatusFromAdapters(ctx context.Contex
 		return nil, errors.GeneralError("Failed to get adapter statuses: %s", err)
 	}
 
-	// Build the list of ConditionAvailable
-	adapters := []openapi.ConditionAvailable{}
-	allReady := true
-	anyFailed := false
+	// Build the list of ResourceCondition
+	adapters := []openapi.ResourceCondition{}
 	maxObservedGeneration := int32(0)
 
 	for _, adapterStatus := range adapterStatuses {
 		// Unmarshal Conditions from JSONB
-		var conditions []openapi.Condition
+		var conditions []openapi.AdapterCondition
 		if err := json.Unmarshal(adapterStatus.Conditions, &conditions); err != nil {
 			continue // Skip if can't unmarshal
 		}
 
 		// Find the "Available" condition
-		var availableCondition *openapi.Condition
+		var availableCondition *openapi.AdapterCondition
 		for i := range conditions {
 			if conditions[i].Type == "Available" {
 				availableCondition = &conditions[i]
@@ -152,61 +150,78 @@ func (s *sqlNodePoolService) UpdateNodePoolStatusFromAdapters(ctx context.Contex
 		}
 
 		if availableCondition == nil {
-			// No Available condition means adapter is not ready
-			allReady = false
+			// No Available condition, skip this adapter
 			continue
 		}
 
-		// Convert to ConditionAvailable
-		condAvail := openapi.ConditionAvailable{
-			Type:               availableCondition.Type,
-			Adapter:            adapterStatus.Adapter,
+		// Convert to ResourceCondition
+		condResource := openapi.ResourceCondition{
+			Type:               MapAdapterToConditionType(adapterStatus.Adapter),
 			Status:             availableCondition.Status,
 			Reason:             availableCondition.Reason,
 			Message:            availableCondition.Message,
-			ObservedGeneration: availableCondition.ObservedGeneration,
+			ObservedGeneration: adapterStatus.ObservedGeneration,
+			LastTransitionTime: availableCondition.LastTransitionTime,
 		}
-		adapters = append(adapters, condAvail)
 
-		// Check status
-		if availableCondition.Status != "True" {
-			allReady = false
-			if availableCondition.Status == "False" {
-				anyFailed = true
-			}
+		// Set CreatedTime with nil check
+		if adapterStatus.CreatedTime != nil {
+			condResource.CreatedTime = *adapterStatus.CreatedTime
+		}
+
+		// Set LastUpdatedTime with nil check
+		if adapterStatus.LastReportTime != nil {
+			condResource.LastUpdatedTime = *adapterStatus.LastReportTime
 		}
 
+		adapters = append(adapters, condResource)
+
 		// Track max observed generation
 		if adapterStatus.ObservedGeneration > maxObservedGeneration {
 			maxObservedGeneration = adapterStatus.ObservedGeneration
 		}
 	}
 
-	// Compute overall phase
-	phase := "NotReady"
-	if len(adapterStatuses) > 0 {
-		if allReady {
-			phase = "Ready"
-		} else if anyFailed {
-			phase = "Failed"
+	// Compute overall phase using required adapters
+	// NodePool doesn't have a generation field, so we pass 0 which means "don't check generation"
+	newPhase := ComputePhase(ctx, adapterStatuses, requiredNodePoolAdapters, 0)
+
+	// Calculate min(adapters[].last_report_time) for nodepool.status.last_updated_time
+	// This uses the OLDEST adapter timestamp to ensure Sentinel can detect stale adapters
+	var minLastUpdatedTime *time.Time
+	for _, adapterStatus := range adapterStatuses {
+		if adapterStatus.LastReportTime != nil {
+			if minLastUpdatedTime == nil || adapterStatus.LastReportTime.Before(*minLastUpdatedTime) {
+				minLastUpdatedTime = adapterStatus.LastReportTime
+			}
 		}
 	}
 
+	// Save old phase to detect transitions
+	oldPhase := nodePool.StatusPhase
+
 	// Update nodepool status fields
 	now := time.Now()
-	nodePool.StatusPhase = phase
+	nodePool.StatusPhase = newPhase
 	nodePool.StatusObservedGeneration = maxObservedGeneration
 
-	// Marshal adapters to JSON
-	adaptersJSON, err := json.Marshal(adapters)
+	// Marshal conditions to JSON
+	conditionsJSON, err := json.Marshal(adapters)
 	if err != nil {
-		return nil, errors.GeneralError("Failed to marshal adapters: %s", err)
+		return nil, errors.GeneralError("Failed to marshal conditions: %s", err)
+	}
+	nodePool.StatusConditions = conditionsJSON
+
+	// Use min(adapters[].last_report_time) instead of now()
+	// This ensures Sentinel triggers reconciliation when ANY adapter is stale
+	if minLastUpdatedTime != nil {
+		nodePool.StatusLastUpdatedTime = minLastUpdatedTime
+	} else {
+		nodePool.StatusLastUpdatedTime = &now
 	}
-	nodePool.StatusAdapters = adaptersJSON
-	nodePool.StatusUpdatedAt = &now
 
-	// Update last transition time if phase changed
-	if nodePool.StatusLastTransitionTime == nil || nodePool.StatusPhase != phase {
+	// Update last transition time only if phase changed
+	if nodePool.StatusLastTransitionTime == nil || oldPhase != newPhase {
 		nodePool.StatusLastTransitionTime = &now
 	}
 
diff --git a/pkg/services/status_aggregation.go b/pkg/services/status_aggregation.go
new file mode 100644
index 00000000..cbe24d8f
--- /dev/null
+++ b/pkg/services/status_aggregation.go
@@ -0,0 +1,174 @@
+package services
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"unicode"
+
+	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
+	"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
+)
+
+// requiredClusterAdapters defines the list of adapters that must be ready for a cluster to be "Ready"
+// All of these adapters must have:
+// 1. available === "True"
+// 2. observed_generation === cluster.generation
+//
+// Based on HyperFleet MVP scope (GCP cluster adapters):
+// - validation: Check GCP prerequisites
+// - dns: Create Cloud DNS records
+// - pullsecret: Store credentials in Secret Manager
+// - hypershift: Create HostedCluster CR
+// Note: placement is NOT required (handled separately)
+var requiredClusterAdapters = []string{
+	"validation",
+	"dns",
+	"pullsecret",
+	"hypershift",
+}
+
+// requiredNodePoolAdapters defines the list of adapters that must be ready for a nodepool to be "Ready"
+// Based on HyperFleet MVP scope (GCP nodepool adapters):
+// - validation: Check nodepool prerequisites
+// - hypershift: Create NodePool CR
+var requiredNodePoolAdapters = []string{
+	"validation",
+	"hypershift",
+}
+
+// adapterConditionSuffixMap allows overriding the default suffix for specific adapters
+// Currently empty - all adapters use "Successful" by default
+// Future example: To make dns use "Ready" instead, uncomment:
+//   "dns": "Ready",
+var adapterConditionSuffixMap = map[string]string{
+	// Add custom mappings here when needed
+}
+
+// MapAdapterToConditionType converts an adapter name to a semantic condition type
+// by converting the adapter name to PascalCase and appending a suffix.
+//
+// Current behavior: All adapters → {AdapterName}Successful
+// Examples:
+//   - "validator" → "ValidatorSuccessful"
+//   - "dns" → "DnsSuccessful"
+//   - "gcp-provisioner" → "GcpProvisionerSuccessful"
+//
+// Future customization: Override suffix in adapterConditionSuffixMap
+//   adapterConditionSuffixMap["dns"] = "Ready" → "DnsReady"
+func MapAdapterToConditionType(adapterName string) string {
+	// Get the suffix for this adapter, default to "Successful"
+	suffix, exists := adapterConditionSuffixMap[adapterName]
+	if !exists {
+		suffix = "Successful"
+	}
+
+	// Convert adapter name to PascalCase
+	// Remove hyphens and capitalize each part
+	parts := strings.Split(adapterName, "-")
+	var result strings.Builder
+
+	for _, part := range parts {
+		if len(part) > 0 {
+			// Capitalize first letter
+			runes := []rune(part)
+			runes[0] = unicode.ToUpper(runes[0])
+			result.WriteString(string(runes))
+		}
+	}
+
+	result.WriteString(suffix)
+	return result.String()
+}
+
+// ComputePhase calculates the overall phase for a resource based on adapter statuses
+// Phase is "Ready" when: numRequiredAdapters == numAdaptersAvailable
+//
+// An adapter is considered "available" when:
+// 1. Available condition status == "True"
+// 2. observed_generation == resource.generation (only checked if resourceGeneration > 0)
+//
+// Returns:
+// - "Ready": Number of required adapters == number of available adapters
+// - "Failed": At least one adapter has status == "False"
+// - "NotReady": Otherwise (missing adapters, stale generation, status == "Unknown", etc.)
+func ComputePhase(ctx context.Context, adapterStatuses api.AdapterStatusList, requiredAdapters []string, resourceGeneration int32) string {
+	log := logger.NewOCMLogger(ctx)
+	if len(adapterStatuses) == 0 {
+		return "NotReady"
+	}
+
+	// Build a map of adapter name -> (available status, observed generation)
+	adapterMap := make(map[string]struct {
+		available          string
+		observedGeneration int32
+	})
+
+	for _, adapterStatus := range adapterStatuses {
+		// Unmarshal conditions to find "Available"
+		var conditions []struct {
+			Type   string `json:"type"`
+			Status string `json:"status"`
+		}
+		if len(adapterStatus.Conditions) > 0 {
+			if err := json.Unmarshal(adapterStatus.Conditions, &conditions); err == nil {
+				for _, cond := range conditions {
+					if cond.Type == "Available" {
+						adapterMap[adapterStatus.Adapter] = struct {
+							available          string
+							observedGeneration int32
+						}{
+							available:          cond.Status,
+							observedGeneration: adapterStatus.ObservedGeneration,
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
+	// Count available adapters and check for failures
+	numAvailable := 0
+	anyFailed := false
+
+	// Iterate over required adapters
+	for _, adapterName := range requiredAdapters {
+		adapterInfo, exists := adapterMap[adapterName]
+
+		if !exists {
+			// Required adapter not found
+			log.Warning(fmt.Sprintf("Required adapter '%s' not found in adapter statuses", adapterName))
+			continue
+		}
+
+		// Check generation matching only if resourceGeneration > 0
+		if resourceGeneration > 0 && adapterInfo.observedGeneration != resourceGeneration {
+			// Adapter is processing old generation (stale)
+			log.Warning(fmt.Sprintf("Required adapter '%s' has stale generation: observed=%d, expected=%d",
+				adapterName, adapterInfo.observedGeneration, resourceGeneration))
+			continue
+		}
+
+		// Check available status
+		if adapterInfo.available == "False" {
+			anyFailed = true
+		} else if adapterInfo.available == "True" {
+			numAvailable++
+		} else {
+			// Status is neither "True" nor "False" (e.g., "Unknown")
+			log.Warning(fmt.Sprintf("Required adapter '%s' has unexpected available status: %s", adapterName, adapterInfo.available))
+		}
+	}
+
+	// Determine phase: Ready when numRequiredAdapters == numAdaptersAvailable
+	numRequired := len(requiredAdapters)
+	if numAvailable == numRequired && numRequired > 0 {
+		return "Ready"
+	}
+	if anyFailed {
+		return "Failed"
+	}
+	return "NotReady"
+}
diff --git a/pkg/services/status_aggregation_test.go b/pkg/services/status_aggregation_test.go
new file mode 100644
index 00000000..56eedc69
--- /dev/null
+++ b/pkg/services/status_aggregation_test.go
@@ -0,0 +1,53 @@
+package services
+
+import "testing"
+
+func TestMapAdapterToConditionType(t *testing.T) {
+	tests := []struct {
+		adapter  string
+		expected string
+	}{
+		{"validator", "ValidatorSuccessful"},
+		{"dns", "DnsSuccessful"},
+		{"gcp-provisioner", "GcpProvisionerSuccessful"},
+		{"unknown-adapter", "UnknownAdapterSuccessful"},
+		{"multi-word-adapter", "MultiWordAdapterSuccessful"},
+		{"single", "SingleSuccessful"},
+	}
+
+	for _, tt := range tests {
+		result := MapAdapterToConditionType(tt.adapter)
+		if result != tt.expected {
+			t.Errorf("MapAdapterToConditionType(%q) = %q, want %q",
+				tt.adapter, result, tt.expected)
+		}
+	}
+}
+
+// Test custom suffix mapping (for future use)
+func TestMapAdapterToConditionType_CustomSuffix(t *testing.T) {
+	// Temporarily add a custom mapping
+	adapterConditionSuffixMap["test-adapter"] = "Ready"
+	defer delete(adapterConditionSuffixMap, "test-adapter")
+
+	result := MapAdapterToConditionType("test-adapter")
+	expected := "TestAdapterReady"
+	if result != expected {
+		t.Errorf("MapAdapterToConditionType(%q) = %q, want %q",
+			"test-adapter", result, expected)
+	}
+}
+
+// Test that default behavior still works after custom suffix is removed
+func TestMapAdapterToConditionType_DefaultAfterCustom(t *testing.T) {
+	// Add and then remove custom mapping
+	adapterConditionSuffixMap["dns"] = "Ready"
+	delete(adapterConditionSuffixMap, "dns")
+
+	result := MapAdapterToConditionType("dns")
+	expected := "DnsSuccessful"
+	if result != expected {
+		t.Errorf("MapAdapterToConditionType(%q) = %q, want %q (should revert to default)",
+			"dns", result, expected)
+	}
+}
diff --git a/test/integration/adapter_status_test.go b/test/integration/adapter_status_test.go
index f442caaa..ab2215f0 100644
--- a/test/integration/adapter_status_test.go
+++ b/test/integration/adapter_status_test.go
@@ -26,15 +26,15 @@ func TestClusterStatusPost(t *testing.T) {
 	statusInput := openapi.AdapterStatusCreateRequest{
 		Adapter:            "test-adapter",
 		ObservedGeneration: cluster.Generation,
-		Conditions: []openapi.Condition{
+		Conditions: []openapi.ConditionRequest{
 			{
 				Type:   "Ready",
 				Status: "True",
 				Reason: openapi.PtrString("AdapterReady"),
 			},
 		},
-		Data: map[string]interface{}{
-			"test_key": "test_value",
+		Data: map[string]map[string]interface{}{
+			"test_key": {"value": "test_value"},
 		},
 	}
 
@@ -62,7 +62,7 @@ func TestClusterStatusGet(t *testing.T) {
 		statusInput := openapi.AdapterStatusCreateRequest{
 			Adapter:            fmt.Sprintf("adapter-%d", i),
 			ObservedGeneration: cluster.Generation,
-			Conditions: []openapi.Condition{
+			Conditions: []openapi.ConditionRequest{
 				{
 					Type:   "Ready",
 					Status: "True",
@@ -99,15 +99,15 @@ func TestNodePoolStatusPost(t *testing.T) {
 	statusInput := openapi.AdapterStatusCreateRequest{
 		Adapter:            "test-nodepool-adapter",
 		ObservedGeneration: 1,
-		Conditions: []openapi.Condition{
+		Conditions: []openapi.ConditionRequest{
 			{
 				Type:   "Ready",
 				Status: "False",
 				Reason: openapi.PtrString("Initializing"),
 			},
 		},
-		Data: map[string]interface{}{
-			"nodepool_data": "value",
+		Data: map[string]map[string]interface{}{
+			"nodepool_data": {"value": "test_value"},
 		},
 	}
 
@@ -135,7 +135,7 @@ func TestNodePoolStatusGet(t *testing.T) {
 		statusInput := openapi.AdapterStatusCreateRequest{
 			Adapter:            fmt.Sprintf("nodepool-adapter-%d", i),
 			ObservedGeneration: 1,
-			Conditions: []openapi.Condition{
+			Conditions: []openapi.ConditionRequest{
 				{
 					Type:   "Ready",
 					Status: "True",
@@ -171,7 +171,7 @@ func TestAdapterStatusPaging(t *testing.T) {
 		statusInput := openapi.AdapterStatusCreateRequest{
 			Adapter:            fmt.Sprintf("adapter-%d", i),
 			ObservedGeneration: cluster.Generation,
-			Conditions: []openapi.Condition{
+			Conditions: []openapi.ConditionRequest{
 				{
 					Type:   "Ready",
 					Status: "True",
@@ -204,15 +204,15 @@ func TestAdapterStatusIdempotency(t *testing.T) {
 	statusInput1 := openapi.AdapterStatusCreateRequest{
 		Adapter:            "idempotency-test-adapter",
 		ObservedGeneration: cluster.Generation,
-		Conditions: []openapi.Condition{
+		Conditions: []openapi.ConditionRequest{
 			{
 				Type:   "Ready",
 				Status: "False",
 				Reason: openapi.PtrString("Initializing"),
 			},
 		},
-		Data: map[string]interface{}{
-			"version": "1.0",
+		Data: map[string]map[string]interface{}{
+			"version": {"value": "1.0"},
 		},
 	}
 
@@ -226,15 +226,15 @@ func TestAdapterStatusIdempotency(t *testing.T) {
 	statusInput2 := openapi.AdapterStatusCreateRequest{
 		Adapter:            "idempotency-test-adapter",
 		ObservedGeneration: cluster.Generation,
-		Conditions: []openapi.Condition{
+		Conditions: []openapi.ConditionRequest{
 			{
 				Type:   "Ready",
 				Status: "True",
 				Reason: openapi.PtrString("AdapterReady"),
 			},
 		},
-		Data: map[string]interface{}{
-			"version": "2.0",
+		Data: map[string]map[string]interface{}{
+			"version": {"value": "2.0"},
 		},
 	}
 
@@ -279,7 +279,7 @@ func TestAdapterStatusPagingEdgeCases(t *testing.T) {
 		statusInput := openapi.AdapterStatusCreateRequest{
 			Adapter:            fmt.Sprintf("edge-adapter-%d", i),
 			ObservedGeneration: cluster.Generation,
-			Conditions: []openapi.Condition{
+			Conditions: []openapi.ConditionRequest{
 				{
 					Type:   "Ready",
 					Status: "True",
@@ -312,7 +312,7 @@ func TestAdapterStatusPagingEdgeCases(t *testing.T) {
 	singleStatus := openapi.AdapterStatusCreateRequest{
 		Adapter:            "single-adapter",
 		ObservedGeneration: singleCluster.Generation,
-		Conditions: []openapi.Condition{
+		Conditions: []openapi.ConditionRequest{
 			{
 				Type:   "Ready",
 				Status: "True",
diff --git a/test/integration/clusters_test.go b/test/integration/clusters_test.go
index 9aa653de..c76ab2fb 100644
--- a/test/integration/clusters_test.go
+++ b/test/integration/clusters_test.go
@@ -38,8 +38,8 @@ func TestClusterGet(t *testing.T) {
 	Expect(*clusterOutput.Id).To(Equal(clusterModel.ID), "found object does not match test object")
 	Expect(clusterOutput.Kind).To(Equal("Cluster"))
 	Expect(*clusterOutput.Href).To(Equal(fmt.Sprintf("/api/hyperfleet/v1/clusters/%s", clusterModel.ID)))
-	Expect(clusterOutput.CreatedAt).To(BeTemporally("~", clusterModel.CreatedAt))
-	Expect(clusterOutput.UpdatedAt).To(BeTemporally("~", clusterModel.UpdatedAt))
+	Expect(clusterOutput.CreatedTime).To(BeTemporally("~", clusterModel.CreatedTime))
+	Expect(clusterOutput.UpdatedTime).To(BeTemporally("~", clusterModel.UpdatedTime))
 }
 
 func TestClusterPost(t *testing.T) {
