diff --git a/client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts b/client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts index 3344ca967..ec8875179 100644 --- a/client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts +++ b/client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts @@ -33,7 +33,7 @@ import type { Message } from "@bufbuild/protobuf"; export const file_fleetmanagement_v1_fleetmanagement: GenFile = /*@__PURE__*/ fileDesc( - "CihmbGVldG1hbmFnZW1lbnQvdjEvZmxlZXRtYW5hZ2VtZW50LnByb3RvEhJmbGVldG1hbmFnZW1lbnQudjEi6gYKEk1pbmVyU3RhdGVTbmFwc2hvdBIZChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC21hY19hZGRyZXNzGAMgASgJEhUKDXNlcmlhbF9udW1iZXIYBCABKAkSKwoLcG93ZXJfdXNhZ2UYBSADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKwoLdGVtcGVyYXR1cmUYBiADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKAoIaGFzaHJhdGUYByADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKgoKZWZmaWNpZW5jeRgIIAMoCzIWLmNvbW1vbi52MS5NZWFzdXJlbWVudBItCgl0aW1lc3RhbXAYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhIKCmlwX2FkZHJlc3MYCiABKAkSCwoDdXJsGAsgASgJEjcKDWRldmljZV9zdGF0dXMYDCABKA4yIC5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU3RhdHVzEjkKDnBhaXJpbmdfc3RhdHVzGA0gASgOMiEuZmxlZXRtYW5hZ2VtZW50LnYxLlBhaXJpbmdTdGF0dXMSDQoFbW9kZWwYDiABKAkSFAoMbWFudWZhY3R1cmVyGA8gASgJEjgKDGNhcGFiaWxpdGllcxgRIAEoCzIiLmNhcGFiaWxpdGllcy52MS5NaW5lckNhcGFiaWxpdGllcxI7ChJ0ZW1wZXJhdHVyZV9zdGF0dXMYEiABKA4yHy50ZWxlbWV0cnkudjEuVGVtcGVyYXR1cmVTdGF0dXMSGAoQZmlybXdhcmVfdmVyc2lvbhgTIAEoCRITCgtkcml2ZXJfbmFtZRgWIAEoCRITCgt3b3JrZXJfbmFtZRgXIAEoCRIVCg1yYWNrX3Bvc2l0aW9uGBggASgJEisKCXBsYWNlbWVudBgcIAEoCzIYLmNvbW1vbi52MS5QbGFjZW1lbnRSZWZzSgQIEBARSgQIFBAVSgQIFRAWSgQIGRAaSgQIGhAbSgQIGxAcUgR0eXBlUgxncm91cF9sYWJlbHNSCnJhY2tfbGFiZWxSB3NpdGVfaWRSCnNpdGVfbGFiZWxSDmJ1aWxkaW5nX2xhYmVsIqkBCh5MaXN0TWluZXJTdGF0ZVNuYXBzaG90c1JlcXVlc3QSHQoJcGFnZV9zaXplGAEgASgFQgq6SAcaBRjoBygAEg4KBmN1cnNvchgCIAEoCRIzCgZmaWx0ZXIYAyABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJMaXN0RmlsdGVyEiMKBHNvcnQYBCADKAsyFS5jb21tb24udjEuU29ydENvbmZpZyKWBAoPTWluZXJMaXN0RmlsdGVyEjcKDWRldmljZV9zdGF0dXMYAyADKA4yIC5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU3RhdHVzEjcKFWVycm9yX2NvbXBvbmVudF90eXBlcxgEIAMoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlEg4KBm1vZGVscxgFIAMoCRI7ChBwYWlyaW5nX3N0YXR1c2VzGAYgAygOMiEuZmxlZXRtYW5hZ2VtZW50LnYxLlBhaXJpbmdTdGF0dXMSEQoJZ3JvdXBfaWRzGAcgAygDEhAKCHJhY2tfaWRzGAggAygDEhkKEWZpcm13YXJlX3ZlcnNpb25zGAkgAygJEhEKBXpvbmVzGAogAygJQgIYARI+Cg5udW1lcmljX3JhbmdlcxgLIAMoCzImLmZsZWV0bWFuYWdlbWVudC52MS5OdW1lcmljUmFuZ2VGaWx0ZXISEAoIaXBfY2lkcnMYDCADKAkSEAoIc2l0ZV9pZHMYDSADKAMSGgoSaW5jbHVkZV91bmFzc2lnbmVkGA4gASgIEhQKDGJ1aWxkaW5nX2lkcxgPIAMoAxIbChNpbmNsdWRlX25vX2J1aWxkaW5nGBAgASgIEiUKCXpvbmVfa2V5cxgRIAMoCzISLmNvbW1vbi52MS5ab25lS2V5EhcKD2luY2x1ZGVfbm9fcmFjaxgSIAEoCCLJAQoSTnVtZXJpY1JhbmdlRmlsdGVyEi8KBWZpZWxkGAEgASgOMiAuZmxlZXRtYW5hZ2VtZW50LnYxLk51bWVyaWNGaWVsZBIpCgNtaW4YAiABKAsyHC5nb29nbGUucHJvdG9idWYuRG91YmxlVmFsdWUSKQoDbWF4GAMgASgLMhwuZ29vZ2xlLnByb3RvYnVmLkRvdWJsZVZhbHVlEhUKDW1pbl9pbmNsdXNpdmUYBCABKAgSFQoNbWF4X2luY2x1c2l2ZRgFIAEoCCLmAQofTGlzdE1pbmVyU3RhdGVTbmFwc2hvdHNSZXNwb25zZRI2CgZtaW5lcnMYASADKAsyJi5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJTdGF0ZVNuYXBzaG90Eg4KBmN1cnNvchgCIAEoCRIUCgx0b3RhbF9taW5lcnMYAyABKAUSOgoSdG90YWxfc3RhdGVfY291bnRzGAQgASgLMh4udGVsZW1ldHJ5LnYxLk1pbmVyU3RhdGVDb3VudHMSDgoGbW9kZWxzGAUgAygJEhkKEWZpcm13YXJlX3ZlcnNpb25zGAYgAygJIioKFFJlZnJlc2hNaW5lcnNSZXF1ZXN0EhIKCmRldmljZV9pZHMYASADKAkiyAEKFVJlZnJlc2hNaW5lcnNSZXNwb25zZRI5CglzbmFwc2hvdHMYASADKAsyJi5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJTdGF0ZVNuYXBzaG90EkUKBmVycm9ycxgCIAMoCzI1LmZsZWV0bWFuYWdlbWVudC52MS5SZWZyZXNoTWluZXJzUmVzcG9uc2UuRXJyb3JzRW50cnkaLQoLRXJyb3JzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKSAQoZRXhwb3J0TWluZXJMaXN0Q3N2UmVxdWVzdBIzCgZmaWx0ZXIYASABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJMaXN0RmlsdGVyEkAKEHRlbXBlcmF0dXJlX3VuaXQYAiABKA4yJi5mbGVldG1hbmFnZW1lbnQudjEuQ3N2VGVtcGVyYXR1cmVVbml0Ii4KGkV4cG9ydE1pbmVyTGlzdENzdlJlc3BvbnNlEhAKCGNzdl9kYXRhGAEgASgMIkoKGkdldE1pbmVyU3RhdGVDb3VudHNSZXF1ZXN0EhAKCHNpdGVfaWRzGAEgAygDEhoKEmluY2x1ZGVfdW5hc3NpZ25lZBgCIAEoCCJpChtHZXRNaW5lclN0YXRlQ291bnRzUmVzcG9uc2USFAoMdG90YWxfbWluZXJzGAEgASgFEjQKDHN0YXRlX2NvdW50cxgCIAEoCzIeLnRlbGVtZXRyeS52MS5NaW5lclN0YXRlQ291bnRzIkMKHkdldE1pbmVyUG9vbEFzc2lnbm1lbnRzUmVxdWVzdBIhChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIGukgDyAEBIlEKDlBvb2xBc3NpZ25tZW50EhQKB3Bvb2xfaWQYASABKANIAIgBARILCgN1cmwYAiABKAkSEAoIdXNlcm5hbWUYAyABKAlCCgoIX3Bvb2xfaWQiVAofR2V0TWluZXJQb29sQXNzaWdubWVudHNSZXNwb25zZRIxCgVwb29scxgBIAMoCzIiLmZsZWV0bWFuYWdlbWVudC52MS5Qb29sQXNzaWdubWVudCJFCg9NaW5lck1vZGVsR3JvdXASDQoFbW9kZWwYASABKAkSFAoMbWFudWZhY3R1cmVyGAIgASgJEg0KBWNvdW50GAMgASgFIlEKGkdldE1pbmVyTW9kZWxHcm91cHNSZXF1ZXN0EjMKBmZpbHRlchgBIAEoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5NaW5lckxpc3RGaWx0ZXIiUgobR2V0TWluZXJNb2RlbEdyb3Vwc1Jlc3BvbnNlEjMKBmdyb3VwcxgBIAMoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5NaW5lck1vZGVsR3JvdXAiPwoaR2V0TWluZXJDb29saW5nTW9kZVJlcXVlc3QSIQoRZGV2aWNlX2lkZW50aWZpZXIYASABKAlCBrpIA8gBASJLChtHZXRNaW5lckNvb2xpbmdNb2RlUmVzcG9uc2USLAoMY29vbGluZ19tb2RlGAEgASgOMhYuY29tbW9uLnYxLkNvb2xpbmdNb2RlIpoBCg5EZXZpY2VTZWxlY3RvchI6CgthbGxfZGV2aWNlcxgBIAEoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5NaW5lckxpc3RGaWx0ZXJIABI6Cg9pbmNsdWRlX2RldmljZXMYAiABKAsyHy5jb21tb24udjEuRGV2aWNlSWRlbnRpZmllckxpc3RIAEIQCg5zZWxlY3Rpb25fdHlwZSJSChNEZWxldGVNaW5lcnNSZXF1ZXN0EjsKD2RldmljZV9zZWxlY3RvchgBIAEoCzIiLmZsZWV0bWFuYWdlbWVudC52MS5EZXZpY2VTZWxlY3RvciItChREZWxldGVNaW5lcnNSZXNwb25zZRIVCg1kZWxldGVkX2NvdW50GAEgASgFIsEBChNSZW5hbWVNaW5lcnNSZXF1ZXN0EkMKD2RldmljZV9zZWxlY3RvchgBIAEoCzIiLmZsZWV0bWFuYWdlbWVudC52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBEkAKC25hbWVfY29uZmlnGAIgASgLMiMuZmxlZXRtYW5hZ2VtZW50LnYxLk1pbmVyTmFtZUNvbmZpZ0IGukgDyAEBEiMKBHNvcnQYAyADKAsyFS5jb21tb24udjEuU29ydENvbmZpZyJcChRSZW5hbWVNaW5lcnNSZXNwb25zZRIVCg1yZW5hbWVkX2NvdW50GAEgASgFEhcKD3VuY2hhbmdlZF9jb3VudBgCIAEoBRIUCgxmYWlsZWRfY291bnQYAyABKAUihgIKGFVwZGF0ZVdvcmtlck5hbWVzUmVxdWVzdBJDCg9kZXZpY2Vfc2VsZWN0b3IYASABKAsyIi5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU2VsZWN0b3JCBrpIA8gBARJACgtuYW1lX2NvbmZpZxgCIAEoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5NaW5lck5hbWVDb25maWdCBrpIA8gBARIjCgRzb3J0GAMgAygLMhUuY29tbW9uLnYxLlNvcnRDb25maWcSHgoNdXNlcl91c2VybmFtZRgEIAEoCUIHukgEcgIQARIeCg11c2VyX3Bhc3N3b3JkGAUgASgJQge6SARyAhABInsKGVVwZGF0ZVdvcmtlck5hbWVzUmVzcG9uc2USFQoNdXBkYXRlZF9jb3VudBgBIAEoBRIXCg91bmNoYW5nZWRfY291bnQYAiABKAUSFAoMZmFpbGVkX2NvdW50GAMgASgFEhgKEGJhdGNoX2lkZW50aWZpZXIYBCABKAkidgoPTWluZXJOYW1lQ29uZmlnEj4KCnByb3BlcnRpZXMYASADKAsyIC5mbGVldG1hbmFnZW1lbnQudjEuTmFtZVByb3BlcnR5Qgi6SAWSAQIIARIjCglzZXBhcmF0b3IYAiABKAlCELpIDXILUgEtUgFfUgEuUgAi0QIKDE5hbWVQcm9wZXJ0eRJKChJzdHJpbmdfYW5kX2NvdW50ZXIYASABKAsyLC5mbGVldG1hbmFnZW1lbnQudjEuU3RyaW5nQW5kQ291bnRlclByb3BlcnR5SAASNgoHY291bnRlchgCIAEoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5Db3VudGVyUHJvcGVydHlIABI6CgxzdHJpbmdfdmFsdWUYAyABKAsyIi5mbGVldG1hbmFnZW1lbnQudjEuU3RyaW5nUHJvcGVydHlIABI9CgtmaXhlZF92YWx1ZRgEIAEoCzImLmZsZWV0bWFuYWdlbWVudC52MS5GaXhlZFZhbHVlUHJvcGVydHlIABI6CglxdWFsaWZpZXIYBSABKAsyJS5mbGVldG1hbmFnZW1lbnQudjEuUXVhbGlmaWVyUHJvcGVydHlIAEIGCgRraW5kInwKGFN0cmluZ0FuZENvdW50ZXJQcm9wZXJ0eRIOCgZwcmVmaXgYASABKAkSDgoGc3VmZml4GAIgASgJEh4KDWNvdW50ZXJfc3RhcnQYAyABKAVCB7pIBBoCKAASIAoNY291bnRlcl9zY2FsZRgEIAEoBUIJukgGGgQYBigBIlMKD0NvdW50ZXJQcm9wZXJ0eRIeCg1jb3VudGVyX3N0YXJ0GAEgASgFQge6SAQaAigAEiAKDWNvdW50ZXJfc2NhbGUYAiABKAVCCbpIBhoEGAYoASIoCg5TdHJpbmdQcm9wZXJ0eRIWCgV2YWx1ZRgBIAEoCUIHukgEcgIQASLyAgoSRml4ZWRWYWx1ZVByb3BlcnR5EjoKBHR5cGUYASABKA4yIi5mbGVldG1hbmFnZW1lbnQudjEuRml4ZWRWYWx1ZVR5cGVCCLpIBYIBAhABEicKD2NoYXJhY3Rlcl9jb3VudBgCIAEoBUIJukgGGgQYBigBSACIAQESRAoHc2VjdGlvbhgDIAEoDjIkLmZsZWV0bWFuYWdlbWVudC52MS5DaGFyYWN0ZXJTZWN0aW9uQgi6SAWCAQIQAUgBiAEBOpABukiMARqJAQolc2VjdGlvbl9yZXF1aXJlZF93aXRoX2NoYXJhY3Rlcl9jb3VudBIvc2VjdGlvbiBpcyByZXF1aXJlZCB3aGVuIGNoYXJhY3Rlcl9jb3VudCBpcyBzZXQaLyFoYXModGhpcy5jaGFyYWN0ZXJfY291bnQpIHx8IGhhcyh0aGlzLnNlY3Rpb24pQhIKEF9jaGFyYWN0ZXJfY291bnRCCgoIX3NlY3Rpb24ibgoRUXVhbGlmaWVyUHJvcGVydHkSOQoEdHlwZRgBIAEoDjIhLmZsZWV0bWFuYWdlbWVudC52MS5RdWFsaWZpZXJUeXBlQgi6SAWCAQIQARIOCgZwcmVmaXgYAiABKAkSDgoGc3VmZml4GAMgASgJKpkBCh9GbGVldE1hbmFnZW1lbnRTZXJ2aWNlRXJyb3JDb2RlEjMKL0ZMRUVUX01BTkFHRU1FTlRfU0VSVklDRV9FUlJPUl9DT0RFX1VOU1BFQ0lGSUVEEAASQQo9RkxFRVRfTUFOQUdFTUVOVF9TRVJWSUNFX0VSUk9SX0NPREVfSU5WQUxJRF9QQUdJTkFUSU9OX0NVUlNPUhABKpoCCgxEZXZpY2VTdGF0dXMSHQoZREVWSUNFX1NUQVRVU19VTlNQRUNJRklFRBAAEhgKFERFVklDRV9TVEFUVVNfT05MSU5FEAESGQoVREVWSUNFX1NUQVRVU19PRkZMSU5FEAISHQoZREVWSUNFX1NUQVRVU19NQUlOVEVOQU5DRRADEhcKE0RFVklDRV9TVEFUVVNfRVJST1IQBBIaChZERVZJQ0VfU1RBVFVTX0lOQUNUSVZFEAUSIwofREVWSUNFX1NUQVRVU19ORUVEU19NSU5JTkdfUE9PTBAGEhoKFkRFVklDRV9TVEFUVVNfVVBEQVRJTkcQBxIhCh1ERVZJQ0VfU1RBVFVTX1JFQk9PVF9SRVFVSVJFRBAIKu0BCg1QYWlyaW5nU3RhdHVzEh4KGlBBSVJJTkdfU1RBVFVTX1VOU1BFQ0lGSUVEEAASGQoVUEFJUklOR19TVEFUVVNfUEFJUkVEEAESGwoXUEFJUklOR19TVEFUVVNfVU5QQUlSRUQQAhIoCiRQQUlSSU5HX1NUQVRVU19BVVRIRU5USUNBVElPTl9ORUVERUQQAxIaChZQQUlSSU5HX1NUQVRVU19QRU5ESU5HEAQSGQoVUEFJUklOR19TVEFUVVNfRkFJTEVEEAUSIwofUEFJUklOR19TVEFUVVNfREVGQVVMVF9QQVNTV09SRBAGKuYBCgxOdW1lcmljRmllbGQSHQoZTlVNRVJJQ19GSUVMRF9VTlNQRUNJRklFRBAAEh4KGk5VTUVSSUNfRklFTERfSEFTSFJBVEVfVEhTEAESIAocTlVNRVJJQ19GSUVMRF9FRkZJQ0lFTkNZX0pUSBACEhoKFk5VTUVSSUNfRklFTERfUE9XRVJfS1cQAxIfChtOVU1FUklDX0ZJRUxEX1RFTVBFUkFUVVJFX0MQBBIbChdOVU1FUklDX0ZJRUxEX1ZPTFRBR0VfVhAFEhsKF05VTUVSSUNfRklFTERfQ1VSUkVOVF9BEAYqgQEKEkNzdlRlbXBlcmF0dXJlVW5pdBIkCiBDU1ZfVEVNUEVSQVRVUkVfVU5JVF9VTlNQRUNJRklFRBAAEiAKHENTVl9URU1QRVJBVFVSRV9VTklUX0NFTFNJVVMQARIjCh9DU1ZfVEVNUEVSQVRVUkVfVU5JVF9GQUhSRU5IRUlUEAIqmQIKDkZpeGVkVmFsdWVUeXBlEiAKHEZJWEVEX1ZBTFVFX1RZUEVfVU5TUEVDSUZJRUQQABIgChxGSVhFRF9WQUxVRV9UWVBFX01BQ19BRERSRVNTEAESIgoeRklYRURfVkFMVUVfVFlQRV9TRVJJQUxfTlVNQkVSEAISIAocRklYRURfVkFMVUVfVFlQRV9XT1JLRVJfTkFNRRADEhoKFkZJWEVEX1ZBTFVFX1RZUEVfTU9ERUwQBBIhCh1GSVhFRF9WQUxVRV9UWVBFX01BTlVGQUNUVVJFUhAFEh0KGUZJWEVEX1ZBTFVFX1RZUEVfTE9DQVRJT04QBhIfChtGSVhFRF9WQUxVRV9UWVBFX01JTkVSX05BTUUQBypuChBDaGFyYWN0ZXJTZWN0aW9uEiEKHUNIQVJBQ1RFUl9TRUNUSU9OX1VOU1BFQ0lGSUVEEAASGwoXQ0hBUkFDVEVSX1NFQ1RJT05fRklSU1QQARIaChZDSEFSQUNURVJfU0VDVElPTl9MQVNUEAIqhwEKDVF1YWxpZmllclR5cGUSHgoaUVVBTElGSUVSX1RZUEVfVU5TUEVDSUZJRUQQABIbChdRVUFMSUZJRVJfVFlQRV9CVUlMRElORxABEhcKE1FVQUxJRklFUl9UWVBFX1JBQ0sQAhIgChxRVUFMSUZJRVJfVFlQRV9SQUNLX1BPU0lUSU9OEAMynwkKFkZsZWV0TWFuYWdlbWVudFNlcnZpY2USggEKF0xpc3RNaW5lclN0YXRlU25hcHNob3RzEjIuZmxlZXRtYW5hZ2VtZW50LnYxLkxpc3RNaW5lclN0YXRlU25hcHNob3RzUmVxdWVzdBozLmZsZWV0bWFuYWdlbWVudC52MS5MaXN0TWluZXJTdGF0ZVNuYXBzaG90c1Jlc3BvbnNlEmQKDVJlZnJlc2hNaW5lcnMSKC5mbGVldG1hbmFnZW1lbnQudjEuUmVmcmVzaE1pbmVyc1JlcXVlc3QaKS5mbGVldG1hbmFnZW1lbnQudjEuUmVmcmVzaE1pbmVyc1Jlc3BvbnNlEnUKEkV4cG9ydE1pbmVyTGlzdENzdhItLmZsZWV0bWFuYWdlbWVudC52MS5FeHBvcnRNaW5lckxpc3RDc3ZSZXF1ZXN0Gi4uZmxlZXRtYW5hZ2VtZW50LnYxLkV4cG9ydE1pbmVyTGlzdENzdlJlc3BvbnNlMAESdgoTR2V0TWluZXJTdGF0ZUNvdW50cxIuLmZsZWV0bWFuYWdlbWVudC52MS5HZXRNaW5lclN0YXRlQ291bnRzUmVxdWVzdBovLmZsZWV0bWFuYWdlbWVudC52MS5HZXRNaW5lclN0YXRlQ291bnRzUmVzcG9uc2USggEKF0dldE1pbmVyUG9vbEFzc2lnbm1lbnRzEjIuZmxlZXRtYW5hZ2VtZW50LnYxLkdldE1pbmVyUG9vbEFzc2lnbm1lbnRzUmVxdWVzdBozLmZsZWV0bWFuYWdlbWVudC52MS5HZXRNaW5lclBvb2xBc3NpZ25tZW50c1Jlc3BvbnNlEnYKE0dldE1pbmVyQ29vbGluZ01vZGUSLi5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJDb29saW5nTW9kZVJlcXVlc3QaLy5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJDb29saW5nTW9kZVJlc3BvbnNlEmEKDERlbGV0ZU1pbmVycxInLmZsZWV0bWFuYWdlbWVudC52MS5EZWxldGVNaW5lcnNSZXF1ZXN0GiguZmxlZXRtYW5hZ2VtZW50LnYxLkRlbGV0ZU1pbmVyc1Jlc3BvbnNlEnYKE0dldE1pbmVyTW9kZWxHcm91cHMSLi5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJNb2RlbEdyb3Vwc1JlcXVlc3QaLy5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJNb2RlbEdyb3Vwc1Jlc3BvbnNlEmEKDFJlbmFtZU1pbmVycxInLmZsZWV0bWFuYWdlbWVudC52MS5SZW5hbWVNaW5lcnNSZXF1ZXN0GiguZmxlZXRtYW5hZ2VtZW50LnYxLlJlbmFtZU1pbmVyc1Jlc3BvbnNlEnAKEVVwZGF0ZVdvcmtlck5hbWVzEiwuZmxlZXRtYW5hZ2VtZW50LnYxLlVwZGF0ZVdvcmtlck5hbWVzUmVxdWVzdBotLmZsZWV0bWFuYWdlbWVudC52MS5VcGRhdGVXb3JrZXJOYW1lc1Jlc3BvbnNlQvABChZjb20uZmxlZXRtYW5hZ2VtZW50LnYxQhRGbGVldG1hbmFnZW1lbnRQcm90b1ABWldnaXRodWIuY29tL2Jsb2NrL3Byb3RvLWZsZWV0L3NlcnZlci9nZW5lcmF0ZWQvZ3JwYy9mbGVldG1hbmFnZW1lbnQvdjE7ZmxlZXRtYW5hZ2VtZW50djGiAgNGWFiqAhJGbGVldG1hbmFnZW1lbnQuVjHKAhJGbGVldG1hbmFnZW1lbnRcVjHiAh5GbGVldG1hbmFnZW1lbnRcVjFcR1BCTWV0YWRhdGHqAhNGbGVldG1hbmFnZW1lbnQ6OlYxYgZwcm90bzM", + "CihmbGVldG1hbmFnZW1lbnQvdjEvZmxlZXRtYW5hZ2VtZW50LnByb3RvEhJmbGVldG1hbmFnZW1lbnQudjEijwcKEk1pbmVyU3RhdGVTbmFwc2hvdBIZChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC21hY19hZGRyZXNzGAMgASgJEhUKDXNlcmlhbF9udW1iZXIYBCABKAkSKwoLcG93ZXJfdXNhZ2UYBSADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKwoLdGVtcGVyYXR1cmUYBiADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKAoIaGFzaHJhdGUYByADKAsyFi5jb21tb24udjEuTWVhc3VyZW1lbnQSKgoKZWZmaWNpZW5jeRgIIAMoCzIWLmNvbW1vbi52MS5NZWFzdXJlbWVudBItCgl0aW1lc3RhbXAYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhIKCmlwX2FkZHJlc3MYCiABKAkSCwoDdXJsGAsgASgJEjcKDWRldmljZV9zdGF0dXMYDCABKA4yIC5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU3RhdHVzEjkKDnBhaXJpbmdfc3RhdHVzGA0gASgOMiEuZmxlZXRtYW5hZ2VtZW50LnYxLlBhaXJpbmdTdGF0dXMSDQoFbW9kZWwYDiABKAkSFAoMbWFudWZhY3R1cmVyGA8gASgJEjgKDGNhcGFiaWxpdGllcxgRIAEoCzIiLmNhcGFiaWxpdGllcy52MS5NaW5lckNhcGFiaWxpdGllcxI7ChJ0ZW1wZXJhdHVyZV9zdGF0dXMYEiABKA4yHy50ZWxlbWV0cnkudjEuVGVtcGVyYXR1cmVTdGF0dXMSGAoQZmlybXdhcmVfdmVyc2lvbhgTIAEoCRITCgtkcml2ZXJfbmFtZRgWIAEoCRITCgt3b3JrZXJfbmFtZRgXIAEoCRIVCg1yYWNrX3Bvc2l0aW9uGBggASgJEisKCXBsYWNlbWVudBgcIAEoCzIYLmNvbW1vbi52MS5QbGFjZW1lbnRSZWZzEiMKG2VtYmVkZGVkX3dlYl92aWV3X2F2YWlsYWJsZRgdIAEoCEoECBAQEUoECBQQFUoECBUQFkoECBkQGkoECBoQG0oECBsQHFIEdHlwZVIMZ3JvdXBfbGFiZWxzUgpyYWNrX2xhYmVsUgdzaXRlX2lkUgpzaXRlX2xhYmVsUg5idWlsZGluZ19sYWJlbCKpAQoeTGlzdE1pbmVyU3RhdGVTbmFwc2hvdHNSZXF1ZXN0Eh0KCXBhZ2Vfc2l6ZRgBIAEoBUIKukgHGgUY6AcoABIOCgZjdXJzb3IYAiABKAkSMwoGZmlsdGVyGAMgASgLMiMuZmxlZXRtYW5hZ2VtZW50LnYxLk1pbmVyTGlzdEZpbHRlchIjCgRzb3J0GAQgAygLMhUuY29tbW9uLnYxLlNvcnRDb25maWcilgQKD01pbmVyTGlzdEZpbHRlchI3Cg1kZXZpY2Vfc3RhdHVzGAMgAygOMiAuZmxlZXRtYW5hZ2VtZW50LnYxLkRldmljZVN0YXR1cxI3ChVlcnJvcl9jb21wb25lbnRfdHlwZXMYBCADKA4yGC5lcnJvcnMudjEuQ29tcG9uZW50VHlwZRIOCgZtb2RlbHMYBSADKAkSOwoQcGFpcmluZ19zdGF0dXNlcxgGIAMoDjIhLmZsZWV0bWFuYWdlbWVudC52MS5QYWlyaW5nU3RhdHVzEhEKCWdyb3VwX2lkcxgHIAMoAxIQCghyYWNrX2lkcxgIIAMoAxIZChFmaXJtd2FyZV92ZXJzaW9ucxgJIAMoCRIRCgV6b25lcxgKIAMoCUICGAESPgoObnVtZXJpY19yYW5nZXMYCyADKAsyJi5mbGVldG1hbmFnZW1lbnQudjEuTnVtZXJpY1JhbmdlRmlsdGVyEhAKCGlwX2NpZHJzGAwgAygJEhAKCHNpdGVfaWRzGA0gAygDEhoKEmluY2x1ZGVfdW5hc3NpZ25lZBgOIAEoCBIUCgxidWlsZGluZ19pZHMYDyADKAMSGwoTaW5jbHVkZV9ub19idWlsZGluZxgQIAEoCBIlCgl6b25lX2tleXMYESADKAsyEi5jb21tb24udjEuWm9uZUtleRIXCg9pbmNsdWRlX25vX3JhY2sYEiABKAgiyQEKEk51bWVyaWNSYW5nZUZpbHRlchIvCgVmaWVsZBgBIAEoDjIgLmZsZWV0bWFuYWdlbWVudC52MS5OdW1lcmljRmllbGQSKQoDbWluGAIgASgLMhwuZ29vZ2xlLnByb3RvYnVmLkRvdWJsZVZhbHVlEikKA21heBgDIAEoCzIcLmdvb2dsZS5wcm90b2J1Zi5Eb3VibGVWYWx1ZRIVCg1taW5faW5jbHVzaXZlGAQgASgIEhUKDW1heF9pbmNsdXNpdmUYBSABKAgi5gEKH0xpc3RNaW5lclN0YXRlU25hcHNob3RzUmVzcG9uc2USNgoGbWluZXJzGAEgAygLMiYuZmxlZXRtYW5hZ2VtZW50LnYxLk1pbmVyU3RhdGVTbmFwc2hvdBIOCgZjdXJzb3IYAiABKAkSFAoMdG90YWxfbWluZXJzGAMgASgFEjoKEnRvdGFsX3N0YXRlX2NvdW50cxgEIAEoCzIeLnRlbGVtZXRyeS52MS5NaW5lclN0YXRlQ291bnRzEg4KBm1vZGVscxgFIAMoCRIZChFmaXJtd2FyZV92ZXJzaW9ucxgGIAMoCSIqChRSZWZyZXNoTWluZXJzUmVxdWVzdBISCgpkZXZpY2VfaWRzGAEgAygJIsgBChVSZWZyZXNoTWluZXJzUmVzcG9uc2USOQoJc25hcHNob3RzGAEgAygLMiYuZmxlZXRtYW5hZ2VtZW50LnYxLk1pbmVyU3RhdGVTbmFwc2hvdBJFCgZlcnJvcnMYAiADKAsyNS5mbGVldG1hbmFnZW1lbnQudjEuUmVmcmVzaE1pbmVyc1Jlc3BvbnNlLkVycm9yc0VudHJ5Gi0KC0Vycm9yc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEikgEKGUV4cG9ydE1pbmVyTGlzdENzdlJlcXVlc3QSMwoGZmlsdGVyGAEgASgLMiMuZmxlZXRtYW5hZ2VtZW50LnYxLk1pbmVyTGlzdEZpbHRlchJAChB0ZW1wZXJhdHVyZV91bml0GAIgASgOMiYuZmxlZXRtYW5hZ2VtZW50LnYxLkNzdlRlbXBlcmF0dXJlVW5pdCIuChpFeHBvcnRNaW5lckxpc3RDc3ZSZXNwb25zZRIQCghjc3ZfZGF0YRgBIAEoDCJKChpHZXRNaW5lclN0YXRlQ291bnRzUmVxdWVzdBIQCghzaXRlX2lkcxgBIAMoAxIaChJpbmNsdWRlX3VuYXNzaWduZWQYAiABKAgiaQobR2V0TWluZXJTdGF0ZUNvdW50c1Jlc3BvbnNlEhQKDHRvdGFsX21pbmVycxgBIAEoBRI0CgxzdGF0ZV9jb3VudHMYAiABKAsyHi50ZWxlbWV0cnkudjEuTWluZXJTdGF0ZUNvdW50cyJDCh5HZXRNaW5lclBvb2xBc3NpZ25tZW50c1JlcXVlc3QSIQoRZGV2aWNlX2lkZW50aWZpZXIYASABKAlCBrpIA8gBASJRCg5Qb29sQXNzaWdubWVudBIUCgdwb29sX2lkGAEgASgDSACIAQESCwoDdXJsGAIgASgJEhAKCHVzZXJuYW1lGAMgASgJQgoKCF9wb29sX2lkIlQKH0dldE1pbmVyUG9vbEFzc2lnbm1lbnRzUmVzcG9uc2USMQoFcG9vbHMYASADKAsyIi5mbGVldG1hbmFnZW1lbnQudjEuUG9vbEFzc2lnbm1lbnQiRQoPTWluZXJNb2RlbEdyb3VwEg0KBW1vZGVsGAEgASgJEhQKDG1hbnVmYWN0dXJlchgCIAEoCRINCgVjb3VudBgDIAEoBSJRChpHZXRNaW5lck1vZGVsR3JvdXBzUmVxdWVzdBIzCgZmaWx0ZXIYASABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJMaXN0RmlsdGVyIlIKG0dldE1pbmVyTW9kZWxHcm91cHNSZXNwb25zZRIzCgZncm91cHMYASADKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJNb2RlbEdyb3VwIj8KGkdldE1pbmVyQ29vbGluZ01vZGVSZXF1ZXN0EiEKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQga6SAPIAQEiSwobR2V0TWluZXJDb29saW5nTW9kZVJlc3BvbnNlEiwKDGNvb2xpbmdfbW9kZRgBIAEoDjIWLmNvbW1vbi52MS5Db29saW5nTW9kZSKaAQoORGV2aWNlU2VsZWN0b3ISOgoLYWxsX2RldmljZXMYASABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJMaXN0RmlsdGVySAASOgoPaW5jbHVkZV9kZXZpY2VzGAIgASgLMh8uY29tbW9uLnYxLkRldmljZUlkZW50aWZpZXJMaXN0SABCEAoOc2VsZWN0aW9uX3R5cGUiUgoTRGVsZXRlTWluZXJzUmVxdWVzdBI7Cg9kZXZpY2Vfc2VsZWN0b3IYASABKAsyIi5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU2VsZWN0b3IiLQoURGVsZXRlTWluZXJzUmVzcG9uc2USFQoNZGVsZXRlZF9jb3VudBgBIAEoBSLBAQoTUmVuYW1lTWluZXJzUmVxdWVzdBJDCg9kZXZpY2Vfc2VsZWN0b3IYASABKAsyIi5mbGVldG1hbmFnZW1lbnQudjEuRGV2aWNlU2VsZWN0b3JCBrpIA8gBARJACgtuYW1lX2NvbmZpZxgCIAEoCzIjLmZsZWV0bWFuYWdlbWVudC52MS5NaW5lck5hbWVDb25maWdCBrpIA8gBARIjCgRzb3J0GAMgAygLMhUuY29tbW9uLnYxLlNvcnRDb25maWciXAoUUmVuYW1lTWluZXJzUmVzcG9uc2USFQoNcmVuYW1lZF9jb3VudBgBIAEoBRIXCg91bmNoYW5nZWRfY291bnQYAiABKAUSFAoMZmFpbGVkX2NvdW50GAMgASgFIoYCChhVcGRhdGVXb3JrZXJOYW1lc1JlcXVlc3QSQwoPZGV2aWNlX3NlbGVjdG9yGAEgASgLMiIuZmxlZXRtYW5hZ2VtZW50LnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQESQAoLbmFtZV9jb25maWcYAiABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuTWluZXJOYW1lQ29uZmlnQga6SAPIAQESIwoEc29ydBgDIAMoCzIVLmNvbW1vbi52MS5Tb3J0Q29uZmlnEh4KDXVzZXJfdXNlcm5hbWUYBCABKAlCB7pIBHICEAESHgoNdXNlcl9wYXNzd29yZBgFIAEoCUIHukgEcgIQASJ7ChlVcGRhdGVXb3JrZXJOYW1lc1Jlc3BvbnNlEhUKDXVwZGF0ZWRfY291bnQYASABKAUSFwoPdW5jaGFuZ2VkX2NvdW50GAIgASgFEhQKDGZhaWxlZF9jb3VudBgDIAEoBRIYChBiYXRjaF9pZGVudGlmaWVyGAQgASgJInYKD01pbmVyTmFtZUNvbmZpZxI+Cgpwcm9wZXJ0aWVzGAEgAygLMiAuZmxlZXRtYW5hZ2VtZW50LnYxLk5hbWVQcm9wZXJ0eUIIukgFkgECCAESIwoJc2VwYXJhdG9yGAIgASgJQhC6SA1yC1IBLVIBX1IBLlIAItECCgxOYW1lUHJvcGVydHkSSgoSc3RyaW5nX2FuZF9jb3VudGVyGAEgASgLMiwuZmxlZXRtYW5hZ2VtZW50LnYxLlN0cmluZ0FuZENvdW50ZXJQcm9wZXJ0eUgAEjYKB2NvdW50ZXIYAiABKAsyIy5mbGVldG1hbmFnZW1lbnQudjEuQ291bnRlclByb3BlcnR5SAASOgoMc3RyaW5nX3ZhbHVlGAMgASgLMiIuZmxlZXRtYW5hZ2VtZW50LnYxLlN0cmluZ1Byb3BlcnR5SAASPQoLZml4ZWRfdmFsdWUYBCABKAsyJi5mbGVldG1hbmFnZW1lbnQudjEuRml4ZWRWYWx1ZVByb3BlcnR5SAASOgoJcXVhbGlmaWVyGAUgASgLMiUuZmxlZXRtYW5hZ2VtZW50LnYxLlF1YWxpZmllclByb3BlcnR5SABCBgoEa2luZCJ8ChhTdHJpbmdBbmRDb3VudGVyUHJvcGVydHkSDgoGcHJlZml4GAEgASgJEg4KBnN1ZmZpeBgCIAEoCRIeCg1jb3VudGVyX3N0YXJ0GAMgASgFQge6SAQaAigAEiAKDWNvdW50ZXJfc2NhbGUYBCABKAVCCbpIBhoEGAYoASJTCg9Db3VudGVyUHJvcGVydHkSHgoNY291bnRlcl9zdGFydBgBIAEoBUIHukgEGgIoABIgCg1jb3VudGVyX3NjYWxlGAIgASgFQgm6SAYaBBgGKAEiKAoOU3RyaW5nUHJvcGVydHkSFgoFdmFsdWUYASABKAlCB7pIBHICEAEi8gIKEkZpeGVkVmFsdWVQcm9wZXJ0eRI6CgR0eXBlGAEgASgOMiIuZmxlZXRtYW5hZ2VtZW50LnYxLkZpeGVkVmFsdWVUeXBlQgi6SAWCAQIQARInCg9jaGFyYWN0ZXJfY291bnQYAiABKAVCCbpIBhoEGAYoAUgAiAEBEkQKB3NlY3Rpb24YAyABKA4yJC5mbGVldG1hbmFnZW1lbnQudjEuQ2hhcmFjdGVyU2VjdGlvbkIIukgFggECEAFIAYgBATqQAbpIjAEaiQEKJXNlY3Rpb25fcmVxdWlyZWRfd2l0aF9jaGFyYWN0ZXJfY291bnQSL3NlY3Rpb24gaXMgcmVxdWlyZWQgd2hlbiBjaGFyYWN0ZXJfY291bnQgaXMgc2V0Gi8haGFzKHRoaXMuY2hhcmFjdGVyX2NvdW50KSB8fCBoYXModGhpcy5zZWN0aW9uKUISChBfY2hhcmFjdGVyX2NvdW50QgoKCF9zZWN0aW9uIm4KEVF1YWxpZmllclByb3BlcnR5EjkKBHR5cGUYASABKA4yIS5mbGVldG1hbmFnZW1lbnQudjEuUXVhbGlmaWVyVHlwZUIIukgFggECEAESDgoGcHJlZml4GAIgASgJEg4KBnN1ZmZpeBgDIAEoCSqZAQofRmxlZXRNYW5hZ2VtZW50U2VydmljZUVycm9yQ29kZRIzCi9GTEVFVF9NQU5BR0VNRU5UX1NFUlZJQ0VfRVJST1JfQ09ERV9VTlNQRUNJRklFRBAAEkEKPUZMRUVUX01BTkFHRU1FTlRfU0VSVklDRV9FUlJPUl9DT0RFX0lOVkFMSURfUEFHSU5BVElPTl9DVVJTT1IQASqaAgoMRGV2aWNlU3RhdHVzEh0KGURFVklDRV9TVEFUVVNfVU5TUEVDSUZJRUQQABIYChRERVZJQ0VfU1RBVFVTX09OTElORRABEhkKFURFVklDRV9TVEFUVVNfT0ZGTElORRACEh0KGURFVklDRV9TVEFUVVNfTUFJTlRFTkFOQ0UQAxIXChNERVZJQ0VfU1RBVFVTX0VSUk9SEAQSGgoWREVWSUNFX1NUQVRVU19JTkFDVElWRRAFEiMKH0RFVklDRV9TVEFUVVNfTkVFRFNfTUlOSU5HX1BPT0wQBhIaChZERVZJQ0VfU1RBVFVTX1VQREFUSU5HEAcSIQodREVWSUNFX1NUQVRVU19SRUJPT1RfUkVRVUlSRUQQCCrtAQoNUGFpcmluZ1N0YXR1cxIeChpQQUlSSU5HX1NUQVRVU19VTlNQRUNJRklFRBAAEhkKFVBBSVJJTkdfU1RBVFVTX1BBSVJFRBABEhsKF1BBSVJJTkdfU1RBVFVTX1VOUEFJUkVEEAISKAokUEFJUklOR19TVEFUVVNfQVVUSEVOVElDQVRJT05fTkVFREVEEAMSGgoWUEFJUklOR19TVEFUVVNfUEVORElORxAEEhkKFVBBSVJJTkdfU1RBVFVTX0ZBSUxFRBAFEiMKH1BBSVJJTkdfU1RBVFVTX0RFRkFVTFRfUEFTU1dPUkQQBirmAQoMTnVtZXJpY0ZpZWxkEh0KGU5VTUVSSUNfRklFTERfVU5TUEVDSUZJRUQQABIeChpOVU1FUklDX0ZJRUxEX0hBU0hSQVRFX1RIUxABEiAKHE5VTUVSSUNfRklFTERfRUZGSUNJRU5DWV9KVEgQAhIaChZOVU1FUklDX0ZJRUxEX1BPV0VSX0tXEAMSHwobTlVNRVJJQ19GSUVMRF9URU1QRVJBVFVSRV9DEAQSGwoXTlVNRVJJQ19GSUVMRF9WT0xUQUdFX1YQBRIbChdOVU1FUklDX0ZJRUxEX0NVUlJFTlRfQRAGKoEBChJDc3ZUZW1wZXJhdHVyZVVuaXQSJAogQ1NWX1RFTVBFUkFUVVJFX1VOSVRfVU5TUEVDSUZJRUQQABIgChxDU1ZfVEVNUEVSQVRVUkVfVU5JVF9DRUxTSVVTEAESIwofQ1NWX1RFTVBFUkFUVVJFX1VOSVRfRkFIUkVOSEVJVBACKpkCCg5GaXhlZFZhbHVlVHlwZRIgChxGSVhFRF9WQUxVRV9UWVBFX1VOU1BFQ0lGSUVEEAASIAocRklYRURfVkFMVUVfVFlQRV9NQUNfQUREUkVTUxABEiIKHkZJWEVEX1ZBTFVFX1RZUEVfU0VSSUFMX05VTUJFUhACEiAKHEZJWEVEX1ZBTFVFX1RZUEVfV09SS0VSX05BTUUQAxIaChZGSVhFRF9WQUxVRV9UWVBFX01PREVMEAQSIQodRklYRURfVkFMVUVfVFlQRV9NQU5VRkFDVFVSRVIQBRIdChlGSVhFRF9WQUxVRV9UWVBFX0xPQ0FUSU9OEAYSHwobRklYRURfVkFMVUVfVFlQRV9NSU5FUl9OQU1FEAcqbgoQQ2hhcmFjdGVyU2VjdGlvbhIhCh1DSEFSQUNURVJfU0VDVElPTl9VTlNQRUNJRklFRBAAEhsKF0NIQVJBQ1RFUl9TRUNUSU9OX0ZJUlNUEAESGgoWQ0hBUkFDVEVSX1NFQ1RJT05fTEFTVBACKocBCg1RdWFsaWZpZXJUeXBlEh4KGlFVQUxJRklFUl9UWVBFX1VOU1BFQ0lGSUVEEAASGwoXUVVBTElGSUVSX1RZUEVfQlVJTERJTkcQARIXChNRVUFMSUZJRVJfVFlQRV9SQUNLEAISIAocUVVBTElGSUVSX1RZUEVfUkFDS19QT1NJVElPThADMp8JChZGbGVldE1hbmFnZW1lbnRTZXJ2aWNlEoIBChdMaXN0TWluZXJTdGF0ZVNuYXBzaG90cxIyLmZsZWV0bWFuYWdlbWVudC52MS5MaXN0TWluZXJTdGF0ZVNuYXBzaG90c1JlcXVlc3QaMy5mbGVldG1hbmFnZW1lbnQudjEuTGlzdE1pbmVyU3RhdGVTbmFwc2hvdHNSZXNwb25zZRJkCg1SZWZyZXNoTWluZXJzEiguZmxlZXRtYW5hZ2VtZW50LnYxLlJlZnJlc2hNaW5lcnNSZXF1ZXN0GikuZmxlZXRtYW5hZ2VtZW50LnYxLlJlZnJlc2hNaW5lcnNSZXNwb25zZRJ1ChJFeHBvcnRNaW5lckxpc3RDc3YSLS5mbGVldG1hbmFnZW1lbnQudjEuRXhwb3J0TWluZXJMaXN0Q3N2UmVxdWVzdBouLmZsZWV0bWFuYWdlbWVudC52MS5FeHBvcnRNaW5lckxpc3RDc3ZSZXNwb25zZTABEnYKE0dldE1pbmVyU3RhdGVDb3VudHMSLi5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJTdGF0ZUNvdW50c1JlcXVlc3QaLy5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJTdGF0ZUNvdW50c1Jlc3BvbnNlEoIBChdHZXRNaW5lclBvb2xBc3NpZ25tZW50cxIyLmZsZWV0bWFuYWdlbWVudC52MS5HZXRNaW5lclBvb2xBc3NpZ25tZW50c1JlcXVlc3QaMy5mbGVldG1hbmFnZW1lbnQudjEuR2V0TWluZXJQb29sQXNzaWdubWVudHNSZXNwb25zZRJ2ChNHZXRNaW5lckNvb2xpbmdNb2RlEi4uZmxlZXRtYW5hZ2VtZW50LnYxLkdldE1pbmVyQ29vbGluZ01vZGVSZXF1ZXN0Gi8uZmxlZXRtYW5hZ2VtZW50LnYxLkdldE1pbmVyQ29vbGluZ01vZGVSZXNwb25zZRJhCgxEZWxldGVNaW5lcnMSJy5mbGVldG1hbmFnZW1lbnQudjEuRGVsZXRlTWluZXJzUmVxdWVzdBooLmZsZWV0bWFuYWdlbWVudC52MS5EZWxldGVNaW5lcnNSZXNwb25zZRJ2ChNHZXRNaW5lck1vZGVsR3JvdXBzEi4uZmxlZXRtYW5hZ2VtZW50LnYxLkdldE1pbmVyTW9kZWxHcm91cHNSZXF1ZXN0Gi8uZmxlZXRtYW5hZ2VtZW50LnYxLkdldE1pbmVyTW9kZWxHcm91cHNSZXNwb25zZRJhCgxSZW5hbWVNaW5lcnMSJy5mbGVldG1hbmFnZW1lbnQudjEuUmVuYW1lTWluZXJzUmVxdWVzdBooLmZsZWV0bWFuYWdlbWVudC52MS5SZW5hbWVNaW5lcnNSZXNwb25zZRJwChFVcGRhdGVXb3JrZXJOYW1lcxIsLmZsZWV0bWFuYWdlbWVudC52MS5VcGRhdGVXb3JrZXJOYW1lc1JlcXVlc3QaLS5mbGVldG1hbmFnZW1lbnQudjEuVXBkYXRlV29ya2VyTmFtZXNSZXNwb25zZULwAQoWY29tLmZsZWV0bWFuYWdlbWVudC52MUIURmxlZXRtYW5hZ2VtZW50UHJvdG9QAVpXZ2l0aHViLmNvbS9ibG9jay9wcm90by1mbGVldC9zZXJ2ZXIvZ2VuZXJhdGVkL2dycGMvZmxlZXRtYW5hZ2VtZW50L3YxO2ZsZWV0bWFuYWdlbWVudHYxogIDRlhYqgISRmxlZXRtYW5hZ2VtZW50LlYxygISRmxlZXRtYW5hZ2VtZW50XFYx4gIeRmxlZXRtYW5hZ2VtZW50XFYxXEdQQk1ldGFkYXRh6gITRmxlZXRtYW5hZ2VtZW50OjpWMWIGcHJvdG8z", [ file_google_protobuf_timestamp, file_google_protobuf_wrappers, @@ -221,6 +221,16 @@ export type MinerStateSnapshot = Message<"fleetmanagement.v1.MinerStateSnapshot" * @generated from field: common.v1.PlacementRefs placement = 28; */ placement?: PlacementRefs | undefined; + + /** + * True when Fleet can open an embedded web management view for this miner. + * Availability is computed from driver support, pairing state, and routing + * ownership; unsupported or fleet-node-owned devices should use their external + * URL or typed control-stream commands instead. + * + * @generated from field: bool embedded_web_view_available = 29; + */ + embeddedWebViewAvailable: boolean; }; /** diff --git a/client/src/protoFleet/components/SingleMinerWrapper/SingleMinerWrapper.tsx b/client/src/protoFleet/components/SingleMinerWrapper/SingleMinerWrapper.tsx index 7cc2a85da..8ac5d100a 100644 --- a/client/src/protoFleet/components/SingleMinerWrapper/SingleMinerWrapper.tsx +++ b/client/src/protoFleet/components/SingleMinerWrapper/SingleMinerWrapper.tsx @@ -1,25 +1,30 @@ -import { ReactNode, useEffect } from "react"; -import { Link, useParams } from "react-router-dom"; +import { motion } from "motion/react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { recallSingleMinerMetadata, type SingleMinerMetadata, type SingleMinerRouteState } from "./routeState"; import { singleMinerRoutePrefetch } from "@/protoFleet/routePrefetch"; import { scopedPath } from "@/protoFleet/routing/siteScope"; import { useFleetStore } from "@/protoFleet/store/useFleetStore"; // eslint-disable-next-line no-restricted-imports -- Fleet shell hosts the protoOS single-miner experience import { MinerHostingProvider } from "@/protoOS/contexts/MinerHostingContext"; -import { DismissCircleDark } from "@/shared/assets/icons"; +import { Dismiss } from "@/shared/assets/icons"; +import Button, { sizes, variants } from "@/shared/components/Button"; +import useSlideUpAnimation from "@/shared/hooks/useSlideUpAnimation"; import { prefetchRoutes } from "@/shared/utils/prefetchRoutes"; -const CloseButton = ({ id }: { id: string }) => { - const activeSite = useFleetStore((state) => state.ui.activeSite); - return ( - - - {id} - - ); -}; +const CloseButton = ({ label, onClose }: { label: string; onClose: () => void }) => ( +
+
+); /** Encode the route param as a single safe path segment. Strips C0 control * characters and whitespace, then re-encodes so /, \, .., ?, # etc. are @@ -27,29 +32,61 @@ const CloseButton = ({ id }: { id: string }) => { // eslint-disable-next-line no-control-regex const safePathSegment = (raw: string): string => encodeURIComponent(raw.replace(/[\x00-\x1f\x7f]/g, "")); +const routeMetadata = (state: unknown): SingleMinerMetadata | undefined => + (state as SingleMinerRouteState | null)?.singleMinerMetadata; + const SingleMinerWrapper = ({ children }: { children: ReactNode }) => { const { id: rawId } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const activeSite = useFleetStore((state) => state.ui.activeSite); + const slideUpAnimation = useSlideUpAnimation(); + const [isClosing, setIsClosing] = useState(false); const safeId = safePathSegment(rawId || ""); const displayId = rawId || ""; + // location.state survives a direct render; the device-keyed cache survives the + // protoOS loader redirects (which drop navigation state). + const cachedMetadata = routeMetadata(location.state) ?? recallSingleMinerMetadata(displayId); + + const metadata = { + minerName: cachedMetadata?.minerName ?? displayId, + ipAddress: cachedMetadata?.ipAddress, + macAddress: cachedMetadata?.macAddress, + firmwareVersion: cachedMetadata?.firmwareVersion, + }; + + const handleClose = useCallback(() => setIsClosing(true), []); - // Once the user is in /miners/:id/*, sibling protoOS chunks (KPI - // tabs, Logs, Diagnostics, per-miner Settings) are one click away; - // warm them at idle so tab switches have no Suspense flash. + // Once the user is in /miners/:id/*, sibling protoOS chunks (KPI tabs, Logs, + // Diagnostics, per-miner Settings) are one click away; warm them at idle so + // tab switches have no Suspense flash. useEffect(() => { return prefetchRoutes(singleMinerRoutePrefetch); }, []); - // Here we are just setting the base url to /:id, - // which vite proxies to the actual miner api server. - // If we wanted to make this request to ProtoFleet backend we - // could pass /miners/:id instead return ( ) as ReactNode} + closeButton={} + mode="fleet" + metadata={metadata} > - {children} + {/* Mirror the full-screen modal: slide/fade in on open, then finish the + exit animation before routing back to the miners list on close. Mounted + on the parent route, so this plays once per visit (not per tab). */} + { + if (isClosing) { + navigate(scopedPath("/fleet/miners", activeSite)); + } + }} + > + {children} + ); }; diff --git a/client/src/protoFleet/components/SingleMinerWrapper/routeState.ts b/client/src/protoFleet/components/SingleMinerWrapper/routeState.ts new file mode 100644 index 000000000..c7855bd42 --- /dev/null +++ b/client/src/protoFleet/components/SingleMinerWrapper/routeState.ts @@ -0,0 +1,45 @@ +import type { MinerStateSnapshot } from "@/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb"; + +export type SingleMinerMetadata = { + minerName?: string; + ipAddress?: string; + macAddress?: string; + firmwareVersion?: string; +}; + +export type SingleMinerRouteState = { + singleMinerMetadata?: SingleMinerMetadata; +}; + +const nonEmpty = (value: string | undefined): string | undefined => { + const normalized = value?.trim(); + return normalized ? normalized : undefined; +}; + +export const buildSingleMinerMetadata = (miner: MinerStateSnapshot): SingleMinerMetadata => ({ + // Match the miner-list name column (MinerName): the device name, falling back + // to its identifier — not the model. + minerName: nonEmpty(miner.name) ?? nonEmpty(miner.deviceIdentifier), + ipAddress: nonEmpty(miner.ipAddress), + macAddress: nonEmpty(miner.macAddress), + firmwareVersion: nonEmpty(miner.firmwareVersion), +}); + +export const buildSingleMinerRouteState = (miner: MinerStateSnapshot): SingleMinerRouteState => ({ + singleMinerMetadata: buildSingleMinerMetadata(miner), +}); + +// The protoOS index routes redirect via loaders (loader: () => redirect(...)), +// which run before render and drop navigation state — so metadata can't ride on +// location.state into SingleMinerWrapper. The opener stamps it here keyed by +// device id (it already holds the list snapshot); the wrapper reads it back. +const metadataByDevice = new Map(); + +export const rememberSingleMinerMetadata = (miner: MinerStateSnapshot): void => { + metadataByDevice.set(miner.deviceIdentifier, buildSingleMinerMetadata(miner)); +}; + +export const recallSingleMinerMetadata = (deviceIdentifier: string): SingleMinerMetadata | undefined => + metadataByDevice.get(deviceIdentifier); + +export const canOpenEmbeddedMinerView = (miner: MinerStateSnapshot): boolean => miner.embeddedWebViewAvailable; diff --git a/client/src/protoFleet/components/SingleMinerWrapper/useOpenMinerView.ts b/client/src/protoFleet/components/SingleMinerWrapper/useOpenMinerView.ts new file mode 100644 index 000000000..df719b097 --- /dev/null +++ b/client/src/protoFleet/components/SingleMinerWrapper/useOpenMinerView.ts @@ -0,0 +1,30 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { buildSingleMinerRouteState, canOpenEmbeddedMinerView, rememberSingleMinerMetadata } from "./routeState"; +import type { MinerStateSnapshot } from "@/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb"; + +/** + * Opens a miner the same way from every entry point (row click, actions menu): + * the embedded single-miner view when the miner can be proxied, otherwise the + * miner's own web UI in a new tab. Centralized so the callers can't drift — + * the embed gate must match what the server proxy can actually serve. + */ +export const useOpenMinerView = () => { + const navigate = useNavigate(); + + return useCallback( + (miner: MinerStateSnapshot) => { + if (canOpenEmbeddedMinerView(miner)) { + rememberSingleMinerMetadata(miner); + navigate(`/miners/${encodeURIComponent(miner.deviceIdentifier)}`, { + state: buildSingleMinerRouteState(miner), + }); + return; + } + if (miner.url) { + window.open(miner.url, "_blank", "noopener,noreferrer"); + } + }, + [navigate], + ); +}; diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.test.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.test.tsx index e429eafbf..92d6f4285 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.test.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.test.tsx @@ -3,13 +3,16 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest"; import { deviceActions, settingsActions } from "./constants"; import SingleMinerActionsMenu from "./SingleMinerActionsMenu"; - -const mockWindowOpen = vi.fn(); -vi.stubGlobal("open", mockWindowOpen); +import type { MinerStateSnapshot } from "@/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb"; const { mockAuthenticateFleetModal, mockBulkActionConfirmDialog, + mockNavigate, + mockOpenMinerView, + mockCompleteBatchOperation, + mockRemoveDevicesFromBatch, + mockStartBatchOperation, mockWithCapabilityCheck, mockPushToast, mockRemoveToast, @@ -28,10 +31,20 @@ const { const mockUpdateSingleWorkerName = vi.fn(); const mockStreamCommandBatchUpdates = vi.fn(); const mockRefreshMiners = vi.fn(); + const mockNavigate = vi.fn(); + const mockOpenMinerView = vi.fn(); + const mockStartBatchOperation = vi.fn(); + const mockCompleteBatchOperation = vi.fn(); + const mockRemoveDevicesFromBatch = vi.fn(); return { mockAuthenticateFleetModal: vi.fn(() => null), mockBulkActionConfirmDialog: vi.fn(() => null), + mockNavigate, + mockOpenMinerView, + mockCompleteBatchOperation, + mockRemoveDevicesFromBatch, + mockStartBatchOperation, mockWithCapabilityCheck, mockPushToast: vi.fn(() => 1), mockRemoveToast: vi.fn(), @@ -124,6 +137,14 @@ vi.mock("@/protoFleet/api/useRefreshMiners", () => ({ }), })); +vi.mock("@/protoFleet/features/fleetManagement/hooks/useBatchOperations", () => ({ + useBatchActions: () => ({ + startBatchOperation: mockStartBatchOperation, + completeBatchOperation: mockCompleteBatchOperation, + removeDevicesFromBatch: mockRemoveDevicesFromBatch, + }), +})); + vi.mock("@/protoFleet/store/hooks/useFleet", () => ({ useMinerDeviceStatus: vi.fn(() => undefined), })); @@ -200,6 +221,14 @@ vi.mock("@/shared/features/toaster", () => ({ }, })); +vi.mock("@/shared/hooks/useNavigate", () => ({ + useNavigate: () => mockNavigate, +})); + +vi.mock("@/protoFleet/components/SingleMinerWrapper/useOpenMinerView", () => ({ + useOpenMinerView: () => mockOpenMinerView, +})); + describe("SingleMinerActionsMenu", () => { beforeEach(() => { vi.clearAllMocks(); @@ -274,12 +303,12 @@ describe("SingleMinerActionsMenu", () => { expect(screen.getByTestId("update-worker-names-popover-button")).toBeInTheDocument(); }); - it("does not render 'View miner' menu item when minerUrl is not provided", () => { + it("renders 'View miner' menu item without requiring minerUrl", () => { render(); fireEvent.click(screen.getByTestId("single-miner-actions-menu-button")); - expect(screen.queryByText("View miner")).not.toBeInTheDocument(); + expect(screen.getByText("View miner")).toBeInTheDocument(); }); it("renders 'View miner' menu item when minerUrl is provided", () => { @@ -291,14 +320,25 @@ describe("SingleMinerActionsMenu", () => { expect(screen.getByTestId("viewMiner-popover-button")).toBeInTheDocument(); }); - it("opens miner URL in new tab when 'View miner' is clicked", () => { - const minerUrl = "http://192.168.1.42"; - render(); + it("opens the row's miner via the shared opener when 'View miner' is clicked", () => { + const miner = { + deviceIdentifier: "my-device-abc", + url: "http://192.168.1.42", + embeddedWebViewAvailable: true, + } as MinerStateSnapshot; + + render( + , + ); fireEvent.click(screen.getByTestId("single-miner-actions-menu-button")); fireEvent.click(screen.getByTestId("viewMiner-popover-button")); - expect(mockWindowOpen).toHaveBeenCalledWith(minerUrl, "_blank", "noopener,noreferrer"); + expect(mockOpenMinerView).toHaveBeenCalledWith(miner); }); it("refreshes a row without calling the full miner refetch callback", async () => { @@ -698,16 +738,16 @@ describe("SingleMinerActionsMenu", () => { return render(); } - it("shows only Unpair when needsAuthentication is true and no minerUrl", () => { + it("shows Unpair and View miner when needsAuthentication is true", () => { renderWithActions({ needsAuthentication: true }); fireEvent.click(screen.getByTestId("single-miner-actions-menu-button")); expect(screen.getByText("Unpair")).toBeInTheDocument(); + expect(screen.getByText("View miner")).toBeInTheDocument(); expect(screen.queryByText("Reboot")).not.toBeInTheDocument(); expect(screen.queryByText("Blink LEDs")).not.toBeInTheDocument(); expect(screen.queryByText("Edit pool")).not.toBeInTheDocument(); - expect(screen.queryByText("View miner")).not.toBeInTheDocument(); expect(screen.queryByTestId("refreshStatus-popover-button")).not.toBeInTheDocument(); }); diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.tsx index fcbf3078b..f8682659c 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.tsx @@ -21,6 +21,7 @@ import type { DeviceStatus } from "@/protoFleet/api/generated/telemetry/v1/telem import { useMinerCommand } from "@/protoFleet/api/useMinerCommand"; import useRefreshMiners from "@/protoFleet/api/useRefreshMiners"; import useUpdateWorkerNames from "@/protoFleet/api/useUpdateWorkerNames"; +import { useOpenMinerView } from "@/protoFleet/components/SingleMinerWrapper/useOpenMinerView"; import AuthenticateFleetModal from "@/protoFleet/features/auth/components/AuthenticateFleetModal"; import { useBatchActions } from "@/protoFleet/features/fleetManagement/hooks/useBatchOperations"; import { ArrowRight, Edit, MiningPools, Plus, Reboot } from "@/shared/assets/icons"; @@ -51,7 +52,6 @@ interface SingleMinerActionsMenuProps { const SingleMinerActionsMenu = ({ deviceIdentifier, - minerUrl, deviceStatus, minerName, workerName, @@ -77,6 +77,7 @@ const SingleMinerActionsMenu = ({ const [reparentKind, setReparentKind] = useState<"rack" | "site" | "building" | null>(null); const [showWarnDialog, setShowWarnDialog] = useState(false); const isRefreshingStatus = refreshing.has(deviceIdentifier); + const openMinerView = useOpenMinerView(); const minerActionsResult = useMinerActions({ selectedMiners, @@ -110,10 +111,11 @@ const SingleMinerActionsMenu = ({ } = minerActionsResult; const handleViewMiner = useCallback(() => { - if (minerUrl) { - window.open(minerUrl, "_blank", "noopener,noreferrer"); + const miner = miners?.[deviceIdentifier]; + if (miner) { + openMinerView(miner); } - }, [minerUrl]); + }, [deviceIdentifier, miners, openMinerView]); const handleRefreshStatus = useCallback(async () => { if (isRefreshingStatus) { @@ -324,16 +326,14 @@ const SingleMinerActionsMenu = ({ ); const actionsWithSingleNameFlows = useMemo(() => { - const viewMinerAction: BulkAction | null = minerUrl - ? { - action: "viewMiner", - title: "View miner", - icon: , - actionHandler: handleViewMiner, - requiresConfirmation: false, - showGroupDivider: true, - } - : null; + const viewMinerAction: BulkAction = { + action: "viewMiner", + title: "View miner", + icon: , + actionHandler: handleViewMiner, + requiresConfirmation: false, + showGroupDivider: true, + }; const renameAction: BulkAction = { action: settingsActions.rename, @@ -358,11 +358,9 @@ const SingleMinerActionsMenu = ({ actionHandler: handleRefreshStatus, requiresConfirmation: false, disabled: isRefreshingStatus, - showGroupDivider: viewMinerAction?.showGroupDivider, + showGroupDivider: viewMinerAction.showGroupDivider, }; - if (viewMinerAction) { - viewMinerAction.showGroupDivider = false; - } + viewMinerAction.showGroupDivider = false; // Inserted before addToGroup so the cluster reads site → building → rack → group. const addToRackAction: BulkAction = { @@ -395,9 +393,7 @@ const SingleMinerActionsMenu = ({ const withAddToSite = insertActionBefore(withAddToBuilding, groupActions.addToBuilding, addToSiteAction); if (actionsWithRenameBeforeGroup !== actions) { - return viewMinerAction - ? [viewMinerAction, refreshStatusAction, ...withAddToSite] - : [refreshStatusAction, ...withAddToSite]; + return [viewMinerAction, refreshStatusAction, ...withAddToSite]; } const actionsWithRenameBeforeSecurity = insertActionBefore(withAddToSite, settingsActions.security, { @@ -406,21 +402,16 @@ const SingleMinerActionsMenu = ({ }); if (actionsWithRenameBeforeSecurity !== withAddToSite) { - return viewMinerAction - ? [viewMinerAction, refreshStatusAction, ...actionsWithRenameBeforeSecurity] - : [refreshStatusAction, ...actionsWithRenameBeforeSecurity]; + return [viewMinerAction, refreshStatusAction, ...actionsWithRenameBeforeSecurity]; } - return viewMinerAction - ? [viewMinerAction, refreshStatusAction, ...withAddToSite, renameAction] - : [refreshStatusAction, ...withAddToSite, renameAction]; + return [viewMinerAction, refreshStatusAction, ...withAddToSite, renameAction]; }, [ handleRefreshStatus, handleRenameOpen, handleUpdateWorkerNameAction, handleViewMiner, isRefreshingStatus, - minerUrl, popoverActions, ]); diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.test.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.test.tsx index 01384e8c2..0639ff618 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.test.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.test.tsx @@ -14,6 +14,7 @@ import { PairingStatus, } from "@/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb"; import { DeviceStatus } from "@/protoFleet/api/generated/telemetry/v1/telemetry_pb"; +import type { SingleMinerRouteState } from "@/protoFleet/components/SingleMinerWrapper/routeState"; import { useFleetStore } from "@/protoFleet/store"; const { mockMinerListActionBar } = vi.hoisted(() => ({ @@ -96,6 +97,7 @@ const createMinerSnapshot = (deviceIdentifier: string, pairingStatus = PairingSt url: "", model: "", firmwareVersion: "", + embeddedWebViewAvailable: false, }); /** Auto-generates miners map from minerIds when miners prop is not provided. */ @@ -120,6 +122,8 @@ const renderMinerList = ( return render( + + , ); }; @@ -130,6 +134,26 @@ const LocationDisplay = () => { return
{location.search}
; }; +const PathDisplay = () => { + const location = useLocation(); + + return
{`${location.pathname}${location.search}`}
; +}; + +const RouteStateDisplay = () => { + const location = useLocation(); + const metadata = (location.state as SingleMinerRouteState | undefined)?.singleMinerMetadata; + + return ( +
+
{metadata?.minerName ?? ""}
+
{metadata?.ipAddress ?? ""}
+
{metadata?.macAddress ?? ""}
+
{metadata?.firmwareVersion ?? ""}
+
+ ); +}; + const isModelColumnVisible = (preferences: { columns: { id: string; visible: boolean }[] }) => preferences.columns.find((column) => column.id === "model")?.visible ?? false; @@ -1110,45 +1134,88 @@ describe("MinerList", () => { }); describe("row click navigation", () => { - it("opens miner URL in a new tab when miner has a URL", async () => { + it("navigates to the embedded single miner route when the embedded web view is available", async () => { + const user = userEvent.setup(); + + const snapshot = createMinerSnapshot("m1"); + snapshot.url = "https://192.168.1.100"; + snapshot.name = "Rig Alpha"; + snapshot.model = "Antminer S21"; + snapshot.ipAddress = "10.0.0.42"; + snapshot.macAddress = "AA:BB:CC:DD:EE:FF"; + snapshot.firmwareVersion = "2026.1"; + snapshot.embeddedWebViewAvailable = true; + + renderMinerList( + { + title: "Miners", + minerIds: ["m1"], + miners: { m1: snapshot }, + totalMiners: 1, + onAddMiners: vi.fn(), + loading: false, + }, + ["/fleet/miners"], + ); + + const row = screen.getByTestId("list-row"); + await user.click(row); + + expect(screen.getByTestId("path-display")).toHaveTextContent("/miners/m1"); + // Matches the list name column (miner.name), not the model. + expect(screen.getByTestId("route-state-miner-name")).toHaveTextContent("Rig Alpha"); + expect(screen.getByTestId("route-state-ip-address")).toHaveTextContent("10.0.0.42"); + expect(screen.getByTestId("route-state-mac-address")).toHaveTextContent("AA:BB:CC:DD:EE:FF"); + expect(screen.getByTestId("route-state-firmware-version")).toHaveTextContent("2026.1"); + }); + + it("opens the miner URL when the embedded web view is unavailable", async () => { const user = userEvent.setup(); const openSpy = vi.spyOn(window, "open").mockImplementation(() => null); const snapshot = createMinerSnapshot("m1"); snapshot.url = "https://192.168.1.100"; - renderMinerList({ - title: "Miners", - minerIds: ["m1"], - miners: { m1: snapshot }, - totalMiners: 1, - onAddMiners: vi.fn(), - loading: false, - }); + renderMinerList( + { + title: "Miners", + minerIds: ["m1"], + miners: { m1: snapshot }, + totalMiners: 1, + onAddMiners: vi.fn(), + loading: false, + }, + ["/fleet/miners"], + ); const row = screen.getByTestId("list-row"); await user.click(row); + expect(screen.getByTestId("path-display")).toHaveTextContent("/fleet/miners"); expect(openSpy).toHaveBeenCalledWith("https://192.168.1.100", "_blank", "noopener,noreferrer"); openSpy.mockRestore(); }); - it("does not open a new tab when miner has no URL", async () => { + it("does nothing when the embedded web view is unavailable and the row has no URL", async () => { const user = userEvent.setup(); const openSpy = vi.spyOn(window, "open").mockImplementation(() => null); - renderMinerList({ - title: "Miners", - minerIds: ["m1"], - miners: { m1: createMinerSnapshot("m1") }, - totalMiners: 1, - onAddMiners: vi.fn(), - loading: false, - }); + renderMinerList( + { + title: "Miners", + minerIds: ["m1"], + miners: { m1: createMinerSnapshot("m1") }, + totalMiners: 1, + onAddMiners: vi.fn(), + loading: false, + }, + ["/fleet/miners"], + ); const row = screen.getByTestId("list-row"); await user.click(row); + expect(screen.getByTestId("path-display")).toHaveTextContent("/fleet/miners"); expect(openSpy).not.toHaveBeenCalled(); openSpy.mockRestore(); }); diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.tsx index 1d8944c1c..ed735e1b2 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.tsx @@ -30,6 +30,7 @@ import { } from "@/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb"; import { DeviceStatus } from "@/protoFleet/api/generated/telemetry/v1/telemetry_pb"; import NoFilterResultsEmptyState from "@/protoFleet/components/NoFilterResultsEmptyState"; +import { useOpenMinerView } from "@/protoFleet/components/SingleMinerWrapper/useOpenMinerView"; import { ProtoFleetStatusModal } from "@/protoFleet/components/StatusModal"; import { PAGE_SCROLL_CHROME_WIDTH } from "@/protoFleet/constants/layout"; import AuthenticateFleetModal from "@/protoFleet/features/auth/components/AuthenticateFleetModal"; @@ -663,11 +664,8 @@ const MinerList = ({ }); }, []); - const handleRowClick = useCallback((item: DeviceListItem) => { - if (item.miner.url) { - window.open(item.miner.url, "_blank", "noopener,noreferrer"); - } - }, []); + const openMinerView = useOpenMinerView(); + const handleRowClick = useCallback((item: DeviceListItem) => openMinerView(item.miner), [openMinerView]); const sortColumnFromUrl = useMemo(() => { const parsedSort = parseSortFromURL(searchParams); return parsedSort ? getColumnForSortField(parsedSort.field) : undefined; diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts b/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts index 21b5cc059..61b90e90d 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts +++ b/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts @@ -20,6 +20,7 @@ export const miners: MinerStateSnapshot[] = [ manufacturer: "Bitmain", workerName: "worker-01", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [ { timestamp: { seconds: BigInt(1641024000), nanos: 0 }, @@ -74,6 +75,7 @@ export const miners: MinerStateSnapshot[] = [ manufacturer: "Bitmain", workerName: "worker-02", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [ { timestamp: { seconds: BigInt(1641024000), nanos: 0 }, @@ -128,6 +130,7 @@ export const miners: MinerStateSnapshot[] = [ manufacturer: "Bitmain", workerName: "worker-03", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [ { timestamp: { seconds: BigInt(1641024000), nanos: 0 }, @@ -182,6 +185,7 @@ export const miners: MinerStateSnapshot[] = [ manufacturer: "Bitmain", workerName: "worker-04", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [ { timestamp: { seconds: BigInt(1641024000), nanos: 0 }, diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts b/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts index 841b363a5..cfb579bef 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts +++ b/client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts @@ -94,6 +94,7 @@ export const hashingMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ONLINE, temperatureStatus: TemperatureStatus.OK, @@ -114,6 +115,7 @@ export const offlineMiner: MinerStateSnapshot = { manufacturer: "Bitmain", workerName: "worker-offline", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [], efficiency: [], powerUsage: [], @@ -138,6 +140,7 @@ export const sleepingMiner: MinerStateSnapshot = { manufacturer: "Bitmain", workerName: "worker-sleeping", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [ { timestamp: { seconds: BigInt(1641283200), nanos: 0 }, @@ -171,6 +174,7 @@ export const authRequiredMiner: MinerStateSnapshot = { manufacturer: "Bitmain", workerName: "worker-auth", driverName: "antminer", + embeddedWebViewAvailable: false, hashrate: [], efficiency: [], powerUsage: [], @@ -194,6 +198,7 @@ export const poolRequiredMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.NEEDS_MINING_POOL, temperatureStatus: TemperatureStatus.OK, @@ -213,6 +218,7 @@ export const controlBoardFailureMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, @@ -232,6 +238,7 @@ export const hashboardFailureMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, @@ -251,6 +258,7 @@ export const psuFailureMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, @@ -270,6 +278,7 @@ export const fanFailureMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, @@ -293,6 +302,7 @@ export const multipleHashboardFailuresMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, @@ -312,6 +322,7 @@ export const multipleComponentFailuresMiner: MinerStateSnapshot = { model: "S19 Pro", manufacturer: "Bitmain", driverName: "antminer", + embeddedWebViewAvailable: false, ...baseMeasurements, deviceStatus: DeviceStatus.ERROR, temperatureStatus: TemperatureStatus.OK, diff --git a/client/src/protoFleet/router.tsx b/client/src/protoFleet/router.tsx index f7d229950..7b3d6a670 100644 --- a/client/src/protoFleet/router.tsx +++ b/client/src/protoFleet/router.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-refresh/only-export-components -- lazy() route components colocated with route config; not HMR-relevant */ -import { createElement, lazy, ReactNode } from "react"; +import { lazy, ReactNode } from "react"; import { createBrowserRouter, LoaderFunction, Navigate, Outlet, redirect } from "react-router-dom"; import App from "./components/App"; @@ -169,18 +169,6 @@ const createScopableRoutes = (absolute: boolean) => [ createRoute(absolute ? "/activity" : "activity", ), ]; -// Wrap protoOS routes with SingleMinerWrapper for /miners/:id/* paths -const wrappedMinerRoutes = singleMinerRoutes.map((route) => { - if (!route.element) return route; - - const wrappedElement = createElement(SingleMinerWrapper, null, route.element); - - return { - ...route, - element: wrappedElement, - }; -}); - /** * Router configuration - defines actual route tree with React elements */ @@ -217,10 +205,19 @@ const router = createBrowserRouter([ createRoute("/sites/:id", , { bg: "surface-5" }), createRoute("/buildings/:id", , { bg: "surface-5" }), - // Single miner (fullscreen - protoOS routes handle layout) + // Single miner (fullscreen - protoOS routes handle layout). SingleMinerWrapper + // wraps the parent Outlet so it stays mounted across tab navigations — the + // protoOS tabs redirect via loaders, which would otherwise remount it (and + // replay its open animation) on every tab. { - ...createRoute("/miners/:id", , { fullscreen: true }), - children: wrappedMinerRoutes, + ...createRoute( + "/miners/:id", + + + , + { fullscreen: true }, + ), + children: singleMinerRoutes, }, // Settings routes diff --git a/client/src/protoOS/api/hooks/useSystemTag.test.ts b/client/src/protoOS/api/hooks/useSystemTag.test.ts new file mode 100644 index 000000000..bcc0f443d --- /dev/null +++ b/client/src/protoOS/api/hooks/useSystemTag.test.ts @@ -0,0 +1,57 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, type Mock, test, vi } from "vitest"; +import { useSystemTag } from "./useSystemTag"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; +import { useAuthRetry } from "@/protoOS/store"; + +const mockGetSystemTag = vi.fn(); +const mockPutSystemTag = vi.fn(); +const mockAuthRetry = vi.fn(); + +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: vi.fn(), +})); + +vi.mock("@/protoOS/store", () => ({ + useAuthRetry: vi.fn(), +})); + +describe("useSystemTag", () => { + beforeEach(() => { + vi.clearAllMocks(); + + (useMinerHosting as Mock).mockReturnValue({ + api: { + getSystemTag: mockGetSystemTag, + putSystemTag: mockPutSystemTag, + }, + }); + (useAuthRetry as Mock).mockReturnValue(mockAuthRetry); + }); + + test("unwraps the firmware tag response object", async () => { + mockGetSystemTag.mockResolvedValue({ data: { tag: "PM-H132435034" } }); + const onSuccess = vi.fn(); + + const { result } = renderHook(() => useSystemTag()); + + result.current.getSystemTag({ onSuccess }); + + await waitFor(() => { + expect(onSuccess).toHaveBeenCalledWith("PM-H132435034"); + }); + }); + + test("treats an empty firmware tag response object as no miner id", async () => { + mockGetSystemTag.mockResolvedValue({ data: { tag: "" } }); + const onSuccess = vi.fn(); + + const { result } = renderHook(() => useSystemTag()); + + result.current.getSystemTag({ onSuccess }); + + await waitFor(() => { + expect(onSuccess).toHaveBeenCalledWith(""); + }); + }); +}); diff --git a/client/src/protoOS/api/hooks/useSystemTag.ts b/client/src/protoOS/api/hooks/useSystemTag.ts index 8dd2c7980..41a0a42d4 100644 --- a/client/src/protoOS/api/hooks/useSystemTag.ts +++ b/client/src/protoOS/api/hooks/useSystemTag.ts @@ -2,6 +2,21 @@ import { useCallback, useMemo } from "react"; import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { useAuthRetry } from "@/protoOS/store"; +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const normalizeSystemTag = (value: unknown): string => { + if (typeof value === "string") { + return value; + } + + if (isRecord(value) && typeof value.tag === "string") { + return value.tag; + } + + return JSON.stringify(value); +}; + const useSystemTag = () => { const { api } = useMinerHosting(); const authRetry = useAuthRetry(); @@ -13,7 +28,7 @@ const useSystemTag = () => { api .getSystemTag() .then((res) => { - onSuccess?.(typeof res.data === "string" ? res.data : JSON.stringify(res.data)); + onSuccess?.(normalizeSystemTag(res.data)); }) .catch((err) => { if (err?.status === 404) { diff --git a/client/src/protoOS/components/App/App.test.tsx b/client/src/protoOS/components/App/App.test.tsx index 030ec3a35..cf8d54aa6 100644 --- a/client/src/protoOS/components/App/App.test.tsx +++ b/client/src/protoOS/components/App/App.test.tsx @@ -6,6 +6,7 @@ import App from "./App"; const mocks = vi.hoisted(() => ({ navigate: vi.fn(), useLocation: vi.fn(), + useMinerHosting: vi.fn(), AuthenticatedShell: vi.fn(), LoginModal: vi.fn(), useMiningStart: vi.fn(), @@ -104,6 +105,10 @@ vi.mock("@/protoOS/components/Power", () => ({ WarnWakeDialog: () => null, })); +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: () => mocks.useMinerHosting(), +})); + vi.mock("@/protoOS/features/auth/components/LoginModal", () => ({ default: (props: { onDismiss: () => void; onSuccess: () => void }) => { mocks.LoginModal(props); @@ -173,6 +178,7 @@ describe("App auth gating", () => { vi.clearAllMocks(); mocks.useLocation.mockReturnValue({ pathname: "/", state: null }); + mocks.useMinerHosting.mockReturnValue({ mode: "direct" }); mocks.useMiningStart.mockReturnValue({ startMining: mocks.startMining }); mocks.useMiningStatus.mockReturnValue({ data: {}, fetchData: mocks.fetchMiningStatus }); mocks.useSystemInfo.mockReturnValue({ reload: mocks.reloadSystemInfo }); @@ -308,6 +314,18 @@ describe("App auth gating", () => { expect(mocks.navigate).not.toHaveBeenCalled(); }); + it("does not render a direct ProtoOS login modal while fleet-hosted", () => { + const setShowLoginModal = vi.fn(); + mocks.useMinerHosting.mockReturnValue({ mode: "fleet" }); + mocks.useShowLoginModal.mockReturnValue(true); + mocks.useSetShowLoginModal.mockReturnValue(setShowLoginModal); + + render(); + + expect(mocks.LoginModal).not.toHaveBeenCalled(); + expect(setShowLoginModal).toHaveBeenCalledWith(false); + }); + it("hides the no-pools callout on the mining pools null state page", () => { // The Pools page renders its own null state when no pools are configured // ("Add up to 3 pools for your miner."). Surfacing the global "No mining diff --git a/client/src/protoOS/components/App/App.tsx b/client/src/protoOS/components/App/App.tsx index 05cdf9f83..c0eaf895e 100644 --- a/client/src/protoOS/components/App/App.tsx +++ b/client/src/protoOS/components/App/App.tsx @@ -16,6 +16,7 @@ import { navigationMenuTypes } from "@/protoOS/components/NavigationMenu"; import NoPoolsCallout from "@/protoOS/components/NoPoolsCallout"; import { getNoPoolsCalloutState } from "@/protoOS/components/NoPoolsCallout/utility"; import { WarnWakeDialog } from "@/protoOS/components/Power"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import LoginModal from "@/protoOS/features/auth/components/LoginModal"; import { isAuthRequiredPath } from "@/protoOS/routeAuth"; import { globalRoutePrefetch } from "@/protoOS/routePrefetch"; @@ -81,6 +82,8 @@ const App = ({ const navigate = useNavigate(); const location = useLocation(); const { pathname } = useMemo(() => location, [location]); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; // Infer if this is an onboarding route from the pathname const isOnboardingRoute = pathname.startsWith("/onboarding"); @@ -99,12 +102,12 @@ const App = ({ const isPasswordSet = usePasswordSet(); const isDefaultPasswordActive = useDefaultPasswordActive(); - const { hasAccess } = useAccessToken(); + const { hasAccess } = useAccessToken(!isFleetHosted); // Require defaultPasswordActive to be explicitly resolved as false before // firing protected hooks. `!== true` would treat `undefined` (status still // loading) as safe, producing a burst of 403s on a factory-password device // during the window between token validation and status resolution. - const canAccessProtectedApi = hasAccess === true && isDefaultPasswordActive === false; + const canAccessProtectedApi = isFleetHosted || (hasAccess === true && isDefaultPasswordActive === false); // Public endpoint — runs regardless of canAccessProtectedApi so bootup // flags stay fresh before the user has authenticated. @@ -123,6 +126,10 @@ const App = ({ // ONBOARDING NAVIGATION // ============================================================================ useEffect(() => { + if (isFleetHosted) { + return; + } + // Only run navigation logic after we have data from the API // undefined means we're still fetching if (isOnboarded !== undefined) { @@ -138,7 +145,15 @@ const App = ({ navigate("/onboarding/authentication"); } } - }, [navigate, isOnboarded, isPasswordSet, isDefaultPasswordActive, isOnboardingRoute, isPasswordChangeRoute]); + }, [ + navigate, + isFleetHosted, + isOnboarded, + isPasswordSet, + isDefaultPasswordActive, + isOnboardingRoute, + isPasswordChangeRoute, + ]); // ============================================================================ // MINING STATUS CHECKING & WAKE LOGIC @@ -203,6 +218,12 @@ const App = ({ const setShowLoginModal = useSetShowLoginModal(); const setDismissedLoginModal = useSetDismissedLoginModal(); + useEffect(() => { + if (isFleetHosted && showLoginModal) { + setShowLoginModal(false); + } + }, [isFleetHosted, setShowLoginModal, showLoginModal]); + const handleDismissLogin = useCallback(() => { // Every auth-required route is render-gated when canAccessProtectedApi is // false, so dismissing from "/" or any settings page would leave the user @@ -253,9 +274,9 @@ const App = ({ // flag flicker (e.g., firmware-update reboot); ESM dedup covers the // re-schedule. useEffect(() => { - if (!isWebServerRunning || !isMiningDriverRunning) return; + if (isFleetHosted || !isWebServerRunning || !isMiningDriverRunning) return; return prefetchRoutes(globalRoutePrefetch); - }, [isWebServerRunning, isMiningDriverRunning]); + }, [isWebServerRunning, isMiningDriverRunning, isFleetHosted]); // ============================================================================ // FIRMWARE UPDATE AUTO-REFRESH AFTER REBOOT @@ -325,13 +346,13 @@ const App = ({ // LOADING STATES // ============================================================================ // Skip the mining driver check during onboarding since the miner may not be fully operational yet - if (!isWebServerRunning || (!isOnboardingRoute && !isMiningDriverRunning)) { + if (!isFleetHosted && (!isWebServerRunning || (!isOnboardingRoute && !isMiningDriverRunning))) { return ; } // Show loading spinner while waiting for system status // undefined = still fetching - if (isOnboarded === undefined) { + if (!isFleetHosted && isOnboarded === undefined) { return (
@@ -341,7 +362,7 @@ const App = ({ // Prevent flash of app UI before redirecting to onboarding // If user needs onboarding and is NOT on an onboarding route, show loading - if (!isOnboarded && !isPasswordSet && !isOnboardingRoute) { + if (!isFleetHosted && !isOnboarded && !isPasswordSet && !isOnboardingRoute) { return (
@@ -353,7 +374,7 @@ const App = ({ // not already on a password-change route. Without this, page-level hooks // that aren't threaded through canAccessProtectedApi (useTelemetry etc.) // would mount and fire a 403 burst before the redirect effect navigates. - if (isDefaultPasswordActive && !isPasswordChangeRoute) { + if (!isFleetHosted && isDefaultPasswordActive && !isPasswordChangeRoute) { return ; } @@ -362,7 +383,7 @@ const App = ({ // route hooks (useTelemetry, useTimeSeries, etc.) fire 401/403 bursts before // LoginModal or the default-password redirect takes over. LoginModal itself // still renders below so the user can recover from an expired session. - const gateRouteChildren = isAuthRequiredPath(pathname) && !canAccessProtectedApi; + const gateRouteChildren = !isFleetHosted && isAuthRequiredPath(pathname) && !canAccessProtectedApi; // ============================================================================ // RENDER @@ -377,7 +398,9 @@ const App = ({
{/* Login Modal - Layout agnostic */} - {showLoginModal ? : null} + {!isFleetHosted && showLoginModal ? ( + + ) : null} {/* Wake Dialog - Layout agnostic */} diff --git a/client/src/protoOS/components/AppLayout/AppLayout.test.tsx b/client/src/protoOS/components/AppLayout/AppLayout.test.tsx new file mode 100644 index 000000000..bf3327914 --- /dev/null +++ b/client/src/protoOS/components/AppLayout/AppLayout.test.tsx @@ -0,0 +1,48 @@ +import { MemoryRouter } from "react-router-dom"; +import { render, screen, within } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; + +import AppLayout from "./AppLayout"; +import { navigationMenuTypes } from "@/protoOS/components/NavigationMenu"; +import { MinerHostingProvider } from "@/protoOS/contexts/MinerHostingContext"; +import useMinerStore from "@/protoOS/store/useMinerStore"; + +describe("AppLayout", () => { + beforeEach(() => { + useMinerStore.getState().systemInfo.setSystemInfo({ + product_name: "Proto Rig", + os: { version: "1.8.0" }, + }); + useMinerStore.getState().networkInfo.setNetworkInfo({ + ip: "192.168.2.14", + mac: "02:00:00:07:3A:11", + }); + }); + + it("uses host metadata for the navigation info panel in fleet-hosted mode", () => { + render( + + + }> +
Content
+
+
+
, + ); + + expect(within(screen.getByTestId("miner-name-info-item")).getByText("Antminer S21")).toBeInTheDocument(); + expect(within(screen.getByTestId("ip-address-info-item")).getByText("10.0.0.42")).toBeInTheDocument(); + expect(within(screen.getByTestId("version-info-item")).getByText("2026.1")).toBeInTheDocument(); + expect(within(screen.getByTestId("mac-address-info-item")).getByText("AA:BB:CC:DD:EE:FF")).toBeInTheDocument(); + + expect(within(screen.getByTestId("miner-name-info-item")).queryByText("Proto Rig")).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/protoOS/components/AppLayout/AppLayout.tsx b/client/src/protoOS/components/AppLayout/AppLayout.tsx index 3b307d438..9f5050775 100644 --- a/client/src/protoOS/components/AppLayout/AppLayout.tsx +++ b/client/src/protoOS/components/AppLayout/AppLayout.tsx @@ -6,6 +6,7 @@ import { ContentLayoutProps } from "@/protoOS/components/ContentLayout/types"; import NavigationMenu, { NavigationMenuType } from "@/protoOS/components/NavigationMenu"; import PageHeader from "@/protoOS/components/PageHeader"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { useIpAddress, useMacAddress, @@ -32,6 +33,8 @@ const AppLayout = ({ ContentLayout = DefaultContentLayout, }: AppLayoutProps) => { const [isMenuOpen, setIsMenuOpen] = useState(false); + const { metadata = {}, mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; // Read system info from store const osVersion = useOSVersion(); @@ -48,22 +51,22 @@ const AppLayout = ({
setIsMenuOpen(false)} versionInfo={{ - value: osVersion, - loading: pendingSystemInfo, + value: isFleetHosted ? metadata.firmwareVersion : osVersion, + loading: isFleetHosted ? false : pendingSystemInfo, }} ipAddressInfo={{ - value: ipAddress, - loading: pendingNetworkInfo, + value: isFleetHosted ? metadata.ipAddress : ipAddress, + loading: isFleetHosted ? false : pendingNetworkInfo, }} minerNameInfo={{ - value: productName, - loading: pendingSystemInfo, + value: isFleetHosted ? metadata.minerName : productName, + loading: isFleetHosted ? false : pendingSystemInfo, }} type={type} /> diff --git a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.test.tsx b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.test.tsx index 495edee53..596751ede 100644 --- a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.test.tsx +++ b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.test.tsx @@ -17,7 +17,7 @@ describe("GlobalActionsWidget", () => { }); test("renders ellipsis button", () => { - const { container } = render( + const { container, getByLabelText, getByTestId } = render( , @@ -25,6 +25,9 @@ describe("GlobalActionsWidget", () => { const button = container.querySelector("button"); expect(button).toBeInTheDocument(); + expect(getByLabelText("Global actions")).toBeInTheDocument(); + expect(getByTestId("global-actions-widget")).toHaveClass("!h-8", "!w-8", "!p-0"); + expect(getByTestId("global-actions-widget").querySelector("svg")?.parentElement).toHaveClass("h-4", "shrink-0"); }); test("opens popover when ellipsis button is clicked", () => { diff --git a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.tsx b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.tsx index 1eb209e93..2f497760b 100644 --- a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.tsx +++ b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.tsx @@ -35,14 +35,14 @@ export const GlobalActionsWidget = ({ onBlinkLEDs, onDownloadLogs }: GlobalActio
setIsOpen((prev) => !prev)} - className="w-[28px] p-0 text-text-primary" + className="!h-8 !w-8 !p-0 text-text-primary" isOpen={isOpen} testId="global-actions-widget" ariaLabel="Global actions" ariaHasPopup="menu" ariaExpanded={isOpen} > - + {isOpen ? : null}
diff --git a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.test.tsx b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.test.tsx index 2a54b9ecf..e2e98df0b 100644 --- a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.test.tsx +++ b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.test.tsx @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, type Mock, test, vi } from "vitest"; import GlobalActionsWidgetWrapper from "./GlobalActionsWidgetWrapper"; import { useDownloadLogs } from "@/protoOS/api/hooks/useDownloadLogs"; import { useLocateSystem } from "@/protoOS/api/hooks/useLocateSystem"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { AUTH_ACTIONS } from "@/protoOS/store/types"; const { mockCheckAccess, mockSetPausedAuthAction, mockSetDismissedLoginModal, mockState } = vi.hoisted(() => ({ @@ -24,6 +25,10 @@ vi.mock("@/protoOS/api/hooks/useDownloadLogs", () => ({ useDownloadLogs: vi.fn(), })); +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: vi.fn(), +})); + vi.mock("@/protoOS/store", async () => { const { AUTH_ACTIONS: actions } = await import("@/protoOS/store/types"); return { @@ -49,6 +54,8 @@ describe("GlobalActionsWidgetWrapper", () => { mockState.pausedAuthAction = null; mockState.dismissedLoginModal = false; + (useMinerHosting as Mock).mockReturnValue({ mode: "direct" }); + (useLocateSystem as Mock).mockReturnValue({ locateSystem: mockLocateSystem, pending: false, @@ -81,6 +88,25 @@ describe("GlobalActionsWidgetWrapper", () => { expect(mockLocateSystem).not.toHaveBeenCalled(); }); + test("calls locateSystem directly when Blink LEDs is clicked in fleet-hosted mode", () => { + (useMinerHosting as Mock).mockReturnValue({ mode: "fleet" }); + + const { container, getByText } = render(); + + const ellipsisButton = container.querySelector("button"); + fireEvent.click(ellipsisButton!); + + const blinkButton = getByText("Blink LEDs").closest("button"); + fireEvent.click(blinkButton!); + + expect(mockSetPausedAuthAction).not.toHaveBeenCalledWith(AUTH_ACTIONS.locate); + expect(mockCheckAccess).not.toHaveBeenCalled(); + expect(mockLocateSystem).toHaveBeenCalledWith({ + ledOnTime: 30, + onError: expect.any(Function), + }); + }); + test("calls locateSystem after auth succeeds", () => { mockState.hasAccess = true; mockState.pausedAuthAction = AUTH_ACTIONS.locate; diff --git a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.tsx b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.tsx index 4a3f54381..d12075b9f 100644 --- a/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.tsx +++ b/client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.tsx @@ -3,6 +3,7 @@ import { GlobalActionsWidget } from "./GlobalActionsWidget"; import type { ErrorProps } from "@/protoOS/api/apiResponseTypes"; import { useDownloadLogs } from "@/protoOS/api/hooks/useDownloadLogs"; import { useLocateSystem } from "@/protoOS/api/hooks/useLocateSystem"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { AUTH_ACTIONS, useAccessToken, @@ -20,13 +21,17 @@ const GlobalActionsWidgetWrapper = () => { const { locateSystem } = useLocateSystem(); const { downloadLogs } = useDownloadLogs(); const [error, setError] = useState(null); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; const dismissedLoginModal = useDismissedLoginModal(); const setDismissedLoginModal = useSetDismissedLoginModal(); const pausedAuthAction = usePausedAuthAction(); const setPausedAuthAction = useSetPausedAuthAction(); - const { checkAccess, hasAccess } = useAccessToken(pausedAuthAction === AUTH_ACTIONS.locate && !dismissedLoginModal); + const { checkAccess, hasAccess } = useAccessToken( + !isFleetHosted && pausedAuthAction === AUTH_ACTIONS.locate && !dismissedLoginModal, + ); // After successful login, retry the locate request useEffect(() => { @@ -48,6 +53,14 @@ const GlobalActionsWidgetWrapper = () => { }, [dismissedLoginModal, setDismissedLoginModal, setPausedAuthAction]); const handleBlinkLEDs = () => { + if (isFleetHosted) { + locateSystem({ + ledOnTime: 30, + onError: (err) => setError(err), + }); + return; + } + setPausedAuthAction(AUTH_ACTIONS.locate); checkAccess(); }; diff --git a/client/src/protoOS/components/PageHeader/PageHeader.test.tsx b/client/src/protoOS/components/PageHeader/PageHeader.test.tsx new file mode 100644 index 000000000..aa61f38d3 --- /dev/null +++ b/client/src/protoOS/components/PageHeader/PageHeader.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import PageHeader from "./PageHeader"; +import { useWindowDimensions } from "@/shared/hooks/useWindowDimensions"; + +vi.mock("@/shared/hooks/useWindowDimensions", () => ({ + useWindowDimensions: vi.fn(), +})); + +vi.mock("./GlobalActions", () => ({ + default: () =>
, +})); + +vi.mock("./MinerStatus", () => ({ + default: () =>
, +})); + +vi.mock("./PoolStatus", () => ({ + default: () =>
, +})); + +vi.mock("./Power", () => ({ + default: () =>
, +})); + +vi.mock("./PowerTarget", () => ({ + default: () =>
, +})); + +vi.mock("@/protoOS/features/firmwareUpdate/components/FirmwareUpdateStatus", () => ({ + default: () =>
, +})); + +describe("PageHeader", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useWindowDimensions).mockReturnValue({ + isDesktop: true, + isLaptop: false, + isPhone: false, + isTablet: false, + width: 1440, + height: 900, + }); + }); + + test("renders miner action widgets", () => { + render(); + + expect(screen.getByTestId("power-target")).toBeInTheDocument(); + expect(screen.getByTestId("pool-status")).toBeInTheDocument(); + expect(screen.getByTestId("power-widget")).toBeInTheDocument(); + expect(screen.getByTestId("global-actions")).toBeInTheDocument(); + }); +}); diff --git a/client/src/protoOS/components/PageHeader/Power/PowerWidget.test.tsx b/client/src/protoOS/components/PageHeader/Power/PowerWidget.test.tsx index 20e559322..dafcb5a5a 100644 --- a/client/src/protoOS/components/PageHeader/Power/PowerWidget.test.tsx +++ b/client/src/protoOS/components/PageHeader/Power/PowerWidget.test.tsx @@ -2,15 +2,38 @@ import { BrowserRouter } from "react-router-dom"; import { fireEvent, render } from "@testing-library/react"; import { beforeEach, describe, expect, type Mock, test, vi } from "vitest"; import PowerWidget from "./PowerWidget"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { useIsAwake, useIsSleeping } from "@/protoOS/store"; import { PopoverProvider } from "@/shared/components/Popover"; +const { mockCheckAccess, mockSetHasAccess, mockSetDismissedLoginModal, mockSetPausedAuthAction, mockState } = + vi.hoisted(() => ({ + mockCheckAccess: vi.fn(), + mockSetHasAccess: vi.fn(), + mockSetDismissedLoginModal: vi.fn(), + mockSetPausedAuthAction: vi.fn(), + mockState: { + dismissedLoginModal: false, + hasAccess: undefined as boolean | undefined, + pausedAuthAction: null as string | null, + }, + })); + vi.mock("@/protoOS/store", async (importOriginal) => { const actual = (await importOriginal()) as any; return { ...actual, useIsAwake: vi.fn(() => true), useIsSleeping: vi.fn(() => false), + useAccessToken: vi.fn(() => ({ + checkAccess: mockCheckAccess, + hasAccess: mockState.hasAccess, + setHasAccess: mockSetHasAccess, + })), + useDismissedLoginModal: vi.fn(() => mockState.dismissedLoginModal), + useSetDismissedLoginModal: vi.fn(() => mockSetDismissedLoginModal), + usePausedAuthAction: vi.fn(() => mockState.pausedAuthAction), + useSetPausedAuthAction: vi.fn(() => mockSetPausedAuthAction), }; }); @@ -32,6 +55,10 @@ vi.mock("@/protoOS/features/auth/contexts/AuthContext", () => ({ })), })); +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: vi.fn(), +})); + describe("Power Widget", () => { const powerButton = "power-button"; const powerPopover = "power-popover"; @@ -47,10 +74,28 @@ describe("Power Widget", () => { beforeEach(() => { vi.clearAllMocks(); + mockState.dismissedLoginModal = false; + mockState.hasAccess = undefined; + mockState.pausedAuthAction = null; + (useMinerHosting as Mock).mockReturnValue({ mode: "direct" }); (useIsAwake as Mock).mockReturnValue(true); (useIsSleeping as Mock).mockReturnValue(false); }); + test("labels the power actions button", () => { + const { getByLabelText, getByTestId } = render( + + + + + , + ); + + expect(getByLabelText("Power actions")).toBeInTheDocument(); + expect(getByTestId(powerButton)).toHaveClass("!h-8", "!w-8", "!p-0"); + expect(getByTestId(powerButton).querySelector("svg")?.parentElement).toHaveClass("h-4", "shrink-0"); + }); + test("renders power widget popover with reboot and sleep if miner is running", () => { (useIsAwake as Mock).mockReturnValue(true); @@ -70,6 +115,23 @@ describe("Power Widget", () => { expect(queryByTestId(popoverWakeUpButton)).not.toBeInTheDocument(); }); + test("opens reboot confirmation directly in fleet-hosted mode", () => { + (useMinerHosting as Mock).mockReturnValue({ mode: "fleet" }); + + const { getByTestId } = render( + + + + + , + ); + + fireEvent.click(getByTestId(powerButton)); + fireEvent.click(getByTestId(popoverRebootButton)); + + expect(getByTestId("warn-reboot-dialog")).toBeInTheDocument(); + }); + test("renders power widget popover with reboot and wake up if miner is stopped", () => { (useIsAwake as Mock).mockReturnValue(false); diff --git a/client/src/protoOS/components/PageHeader/Power/PowerWidget.tsx b/client/src/protoOS/components/PageHeader/Power/PowerWidget.tsx index 03025cfaf..91cb3a62b 100644 --- a/client/src/protoOS/components/PageHeader/Power/PowerWidget.tsx +++ b/client/src/protoOS/components/PageHeader/Power/PowerWidget.tsx @@ -4,6 +4,7 @@ import WidgetWrapper from "../WidgetWrapper"; import PowerPopover from "./PowerPopover"; import { ErrorProps } from "@/protoOS/api/apiResponseTypes"; import { EnteringSleepDialog, RebootingDialog, WarnRebootDialog, WarnSleepDialog } from "@/protoOS/components/Power"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { useAccessToken } from "@/protoOS/store"; import { AUTH_ACTIONS, @@ -46,6 +47,8 @@ const PowerWidget = ({ const { triggerRef: WidgetRef } = useResponsivePopover(); const isAwake = useIsAwake(); const isSleeping = useIsSleeping(); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; const [isOpen, setIsOpen] = useState(shouldShowPopover); const [warnReboot, setWarnReboot] = useState(false); @@ -57,7 +60,9 @@ const PowerWidget = ({ const pausedAuthAction = usePausedAuthAction(); const setPausedAuthAction = useSetPausedAuthAction(); - const { checkAccess, hasAccess, setHasAccess } = useAccessToken(!!pausedAuthAction && !dismissedLoginModal); + const { checkAccess, hasAccess, setHasAccess } = useAccessToken( + !isFleetHosted && !!pausedAuthAction && !dismissedLoginModal, + ); const onClickOutside = useCallback(() => { setIsOpen(false); @@ -101,6 +106,10 @@ const PowerWidget = ({ const handleRebootButton = () => { setIsOpen(false); + if (isFleetHosted) { + setWarnReboot(true); + return; + } setPausedAuthAction(AUTH_ACTIONS.reboot); checkAccess(); }; @@ -113,6 +122,10 @@ const PowerWidget = ({ const handleSleepButton = () => { setIsOpen(false); + if (isFleetHosted) { + setWarnSleep(true); + return; + } setPausedAuthAction(AUTH_ACTIONS.sleep); checkAccess(); }; @@ -166,11 +179,14 @@ const PowerWidget = ({
setIsOpen((prev) => !prev)} - className="w-[28px] p-0 text-text-primary" + className="!h-8 !w-8 !p-0 text-text-primary" isOpen={isOpen} testId="power-button" + ariaLabel="Power actions" + ariaHasPopup="menu" + ariaExpanded={isOpen} > - + {isOpen ? ( diff --git a/client/src/protoOS/contexts/MinerHostingContext/MinerHostingContext.tsx b/client/src/protoOS/contexts/MinerHostingContext/MinerHostingContext.tsx index 69011c8eb..383d6b44d 100644 --- a/client/src/protoOS/contexts/MinerHostingContext/MinerHostingContext.tsx +++ b/client/src/protoOS/contexts/MinerHostingContext/MinerHostingContext.tsx @@ -2,6 +2,19 @@ import { createContext, ReactNode, useMemo } from "react"; import { Api, RequestParams } from "@/protoOS/api/generatedApi"; import useMinerStore from "@/protoOS/store/useMinerStore"; +export type MinerHostingMode = "direct" | "fleet"; + +export type MinerHostingMetadata = { + minerName?: string; + ipAddress?: string; + macAddress?: string; + firmwareVersion?: string; +}; + +// Stable identity for the absent-metadata case so consumers' memoization +// (useMinerHosting) isn't busted by a fresh `{}` on every provider render. +const EMPTY_METADATA: MinerHostingMetadata = {}; + // Read the access token at request time so every call picks up the latest // value from the store. Using setSecurityData from a useEffect races against // child useEffects (which fire first on initial mount), so the first fetch @@ -16,20 +29,17 @@ const securityWorker = (): RequestParams | void => { }; }; -const CreateApi = (baseUrl: string) => { - const url = (baseUrl.length ? "/" : "") + baseUrl; +const CreateApi = (baseUrl: string, mode: MinerHostingMode) => { + const url = baseUrl.length ? (baseUrl.startsWith("/") ? baseUrl : `/${baseUrl}`) : ""; const instance = new Api({ baseUrl: url, - securityWorker, + securityWorker: mode === "direct" ? securityWorker : undefined, // Require auth on all requests by default; firmware now gates nearly every // endpoint. Callers of truly public endpoints (system/status, pairing/info) // must pass { secure: false } to opt out. baseApiParams: { secure: true }, }); - // TODO: remove this when done with development - (window as any).api = instance.api; - return instance; }; @@ -39,12 +49,16 @@ type MinerHostingContextType = { api: ApiT | null; minerRoot: string; closeButton: ReactNode | null; + mode: MinerHostingMode; + metadata: MinerHostingMetadata; }; const MinerHostingContext = createContext({ api: null, minerRoot: "", closeButton: null, + mode: "direct", + metadata: EMPTY_METADATA, }); type MinerHostingProviderProps = { @@ -52,6 +66,8 @@ type MinerHostingProviderProps = { baseUrl?: string; minerRoot?: string; closeButton?: ReactNode | null; + mode?: MinerHostingMode; + metadata?: MinerHostingMetadata; }; export const MinerHostingProvider = ({ @@ -59,12 +75,16 @@ export const MinerHostingProvider = ({ baseUrl = "", minerRoot = "", closeButton = null, + mode = "direct", + metadata = EMPTY_METADATA, }: MinerHostingProviderProps) => { - const instance = useMemo(() => CreateApi(baseUrl), [baseUrl]); + const instance = useMemo(() => CreateApi(baseUrl, mode), [baseUrl, mode]); const api = instance.api; return ( - {children} + + {children} + ); }; diff --git a/client/src/protoOS/contexts/MinerHostingContext/useMinerHosting.ts b/client/src/protoOS/contexts/MinerHostingContext/useMinerHosting.ts index a33fcd0ea..f99e7199f 100644 --- a/client/src/protoOS/contexts/MinerHostingContext/useMinerHosting.ts +++ b/client/src/protoOS/contexts/MinerHostingContext/useMinerHosting.ts @@ -2,7 +2,10 @@ import { useContext, useMemo } from "react"; import MinerHostingContext from "./MinerHostingContext"; export const useMinerHosting = () => { - const { api, minerRoot, closeButton } = useContext(MinerHostingContext); + const { api, minerRoot, closeButton, mode, metadata } = useContext(MinerHostingContext); - return useMemo(() => ({ api, minerRoot, closeButton }), [api, minerRoot, closeButton]); + return useMemo( + () => ({ api, minerRoot, closeButton, mode, metadata }), + [api, minerRoot, closeButton, mode, metadata], + ); }; diff --git a/client/src/protoOS/features/kpis/components/Efficiency/Efficiency.tsx b/client/src/protoOS/features/kpis/components/Efficiency/Efficiency.tsx index 88c517b23..59577c0db 100644 --- a/client/src/protoOS/features/kpis/components/Efficiency/Efficiency.tsx +++ b/client/src/protoOS/features/kpis/components/Efficiency/Efficiency.tsx @@ -52,20 +52,20 @@ const Efficiency = () => { const aggregates = miner?.efficiency?.timeSeries?.aggregates; const lowestPerformer = useMemo(() => { - if (!hashboards) return undefined; + if (hashboards.length === 0) return undefined; let lowestSlot: number | undefined; let lowestAvg = -Infinity; // For efficiency, lower is worse, so we want highest value (worst efficiency) hashboards.forEach((hashboard) => { const hashboardAvg = hashboard.efficiency?.timeSeries?.aggregates?.avg?.value; - if (!!hashboardAvg && hashboardAvg > lowestAvg) { + if (typeof hashboardAvg === "number" && hashboardAvg > lowestAvg) { lowestAvg = hashboardAvg; lowestSlot = useMinerStore.getState().hardware.getSlotByHbSn(hashboard.serial); } }); - return lowestSlot ? "Hashboard " + lowestSlot : undefined; + return lowestSlot === undefined ? undefined : "Hashboard " + lowestSlot; }, [hashboards]); return ( diff --git a/client/src/protoOS/features/kpis/components/Hashrate/Hashrate.tsx b/client/src/protoOS/features/kpis/components/Hashrate/Hashrate.tsx index 29feaf54a..6afb513cd 100644 --- a/client/src/protoOS/features/kpis/components/Hashrate/Hashrate.tsx +++ b/client/src/protoOS/features/kpis/components/Hashrate/Hashrate.tsx @@ -52,20 +52,20 @@ const Hashrate = () => { const aggregates = miner?.hashrate?.timeSeries?.aggregates; const lowestPerformer = useMemo(() => { - if (!hashboards) return undefined; + if (hashboards.length === 0) return undefined; let lowestSlot: number | undefined; let lowestAvg = Infinity; hashboards.forEach((hashboard) => { const hashboardAvg = hashboard.hashrate?.timeSeries?.aggregates?.avg?.value; - if (!!hashboardAvg && hashboardAvg < lowestAvg) { + if (typeof hashboardAvg === "number" && hashboardAvg < lowestAvg) { lowestAvg = hashboardAvg; lowestSlot = useMinerStore.getState().hardware.getSlotByHbSn(hashboard.serial); } }); - return lowestSlot ? "Hashboard " + lowestSlot : undefined; + return lowestSlot === undefined ? undefined : "Hashboard " + lowestSlot; }, [hashboards]); return ( diff --git a/client/src/protoOS/features/kpis/components/Temperature/Temperature.tsx b/client/src/protoOS/features/kpis/components/Temperature/Temperature.tsx index 3cf21adeb..6aae374b5 100644 --- a/client/src/protoOS/features/kpis/components/Temperature/Temperature.tsx +++ b/client/src/protoOS/features/kpis/components/Temperature/Temperature.tsx @@ -59,20 +59,20 @@ const Temperature = () => { const aggregates = miner?.temperature?.timeSeries?.aggregates; const lowestPerformer = useMemo(() => { - if (!hashboards) return undefined; + if (hashboards.length === 0) return undefined; let hottestSlot: number | undefined; let hottestTemp = -Infinity; hashboards.forEach((hashboard) => { const hashboardMax = hashboard.temperature?.timeSeries?.aggregates?.max?.value; - if (!!hashboardMax && hashboardMax > hottestTemp) { + if (typeof hashboardMax === "number" && hashboardMax > hottestTemp) { hottestTemp = hashboardMax; hottestSlot = useMinerStore.getState().hardware.getSlotByHbSn(hashboard.serial); } }); - return hottestSlot ? "Hashboard " + hottestSlot : undefined; + return hottestSlot === undefined ? undefined : "Hashboard " + hottestSlot; }, [hashboards]); return ( diff --git a/client/src/protoOS/features/settings/components/Cooling/Cooling.test.tsx b/client/src/protoOS/features/settings/components/Cooling/Cooling.test.tsx new file mode 100644 index 000000000..a26d2d723 --- /dev/null +++ b/client/src/protoOS/features/settings/components/Cooling/Cooling.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import Cooling from "./Cooling"; +import { useCoolingStatus } from "@/protoOS/api"; +import { useCoolingMode, useFansTelemetry, useIsSleeping } from "@/protoOS/store"; + +vi.mock("@/protoOS/api", () => ({ + useCoolingStatus: vi.fn(), +})); + +vi.mock("@/protoOS/store", () => ({ + useCoolingMode: vi.fn(), + useFansTelemetry: vi.fn(), + useIsSleeping: vi.fn(), +})); + +vi.mock("@/protoOS/components/Power", () => ({ + EnteringSleepDialog: () => null, +})); + +vi.mock("@/shared/features/toaster", () => ({ + pushToast: vi.fn(() => 1), + updateToast: vi.fn(), +})); + +describe("Cooling", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useCoolingStatus).mockReturnValue({ + data: undefined, + error: undefined, + loaded: true, + pending: false, + setCooling: vi.fn(), + } as ReturnType); + vi.mocked(useFansTelemetry).mockReturnValue([]); + vi.mocked(useIsSleeping).mockReturnValue(false); + }); + + test("selects air cooled when the store already has Auto mode on mount", () => { + vi.mocked(useCoolingMode).mockReturnValue("Auto"); + + render(); + + expect(screen.getByTestId("cooling-option-air").querySelector("input")).toBeChecked(); + expect(screen.getByTestId("cooling-option-immersion").querySelector("input")).not.toBeChecked(); + }); + + test("selects immersion cooled when the store already has Off mode on mount", () => { + vi.mocked(useCoolingMode).mockReturnValue("Off"); + + render(); + + expect(screen.getByTestId("cooling-option-air").querySelector("input")).not.toBeChecked(); + expect(screen.getByTestId("cooling-option-immersion").querySelector("input")).toBeChecked(); + }); +}); diff --git a/client/src/protoOS/features/settings/components/Cooling/Cooling.tsx b/client/src/protoOS/features/settings/components/Cooling/Cooling.tsx index c5505052e..b7f03e967 100644 --- a/client/src/protoOS/features/settings/components/Cooling/Cooling.tsx +++ b/client/src/protoOS/features/settings/components/Cooling/Cooling.tsx @@ -55,6 +55,13 @@ const isAirCooledMode = (fanMode: string | undefined) => fanMode === "Auto" || f const isImmersionMode = (fanMode: string | undefined) => fanMode === "Off"; +const coolingModeFromFanMode = (fanMode: string | undefined | null): CoolingMode | undefined => { + if (!fanMode) return undefined; + if (isAirCooledMode(fanMode)) return COOLING_MODES.air; + if (isImmersionMode(fanMode)) return COOLING_MODES.immersion; + return undefined; +}; + const disabledClassName = "opacity-50 pointer-events-none"; const isSelected = ( @@ -71,13 +78,15 @@ const isSelected = ( const Cooling = () => { const { pending, setCooling } = useCoolingStatus({ poll: true }); - const [coolingMode, setCoolingMode] = useState(); const isSleeping = useIsSleeping(); const storeCoolingMode = useCoolingMode(); + const [coolingMode, setCoolingMode] = useState(() => + coolingModeFromFanMode(storeCoolingMode), + ); const fans = useFansTelemetry(); const [userSelectedCoolingMode, setUserSelectedCoolingMode] = useState(); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(() => coolingModeFromFanMode(storeCoolingMode) === undefined); const [showImmersionModal, setShowImmersionModal] = useState(false); const [showLearnMoreModal, setShowLearnMoreModal] = useState(false); const [showSleepDialog, setShowSleepDialog] = useState(false); @@ -86,14 +95,10 @@ const Cooling = () => { const [prevStoreCoolingMode, setPrevStoreCoolingMode] = useState(storeCoolingMode); if (prevStoreCoolingMode !== storeCoolingMode) { setPrevStoreCoolingMode(storeCoolingMode); - if (storeCoolingMode) { - if (isAirCooledMode(storeCoolingMode)) { - setCoolingMode(COOLING_MODES.air); - setLoading(false); - } else if (isImmersionMode(storeCoolingMode)) { - setCoolingMode(COOLING_MODES.immersion); - setLoading(false); - } + const nextCoolingMode = coolingModeFromFanMode(storeCoolingMode); + if (nextCoolingMode) { + setCoolingMode(nextCoolingMode); + setLoading(false); } } diff --git a/client/src/protoOS/hooks/useWakeMiner/useWakeMiner.ts b/client/src/protoOS/hooks/useWakeMiner/useWakeMiner.ts index 815a6b689..e9fb9d80c 100644 --- a/client/src/protoOS/hooks/useWakeMiner/useWakeMiner.ts +++ b/client/src/protoOS/hooks/useWakeMiner/useWakeMiner.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useMiningStart, useMiningStatus } from "@/protoOS/api"; import { ErrorProps } from "@/protoOS/api/apiResponseTypes"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { useAccessToken } from "@/protoOS/store"; import { AUTH_ACTIONS, @@ -34,7 +35,9 @@ export const useWakeMiner = ({ afterWake, onSuccess, onError }: UseWakeMinerProp const setDismissedLoginModal = useSetDismissedLoginModal(); const pausedAuthAction = usePausedAuthAction(); const setPausedAuthAction = useSetPausedAuthAction(); - const { checkAccess, hasAccess } = useAccessToken(!!pausedAuthAction && !dismissedLoginModal); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; + const { checkAccess, hasAccess } = useAccessToken(!isFleetHosted && !!pausedAuthAction && !dismissedLoginModal); const afterWakeRef = useRef(afterWake); const onSuccessRef = useRef(onSuccess); const onErrorRef = useRef(onError); @@ -146,9 +149,14 @@ export const useWakeMiner = ({ afterWake, onSuccess, onError }: UseWakeMinerProp }, [hasAccess, pausedAuthAction, setPausedAuthAction, showWakeDialog, hideWakeDialog, handleWakeConfirm]); const wakeMiner = useCallback(() => { + if (isFleetHosted) { + showWakeDialog(handleWakeConfirm, () => hideWakeDialog()); + return; + } + setPausedAuthAction(AUTH_ACTIONS.wake); checkAccess(); - }, [checkAccess, setPausedAuthAction]); + }, [checkAccess, handleWakeConfirm, hideWakeDialog, isFleetHosted, setPausedAuthAction, showWakeDialog]); return { wakeMiner, diff --git a/client/src/protoOS/store/hooks/useAuth.test.ts b/client/src/protoOS/store/hooks/useAuth.test.ts index a7af8e1b0..47f3c03d6 100644 --- a/client/src/protoOS/store/hooks/useAuth.test.ts +++ b/client/src/protoOS/store/hooks/useAuth.test.ts @@ -6,6 +6,7 @@ const mockRefresh = vi.fn(); const mockLogout = vi.fn(); const mockSetShowLoginModal = vi.fn(); const mockUseLocation = vi.hoisted(() => vi.fn()); +const mockUseMinerHosting = vi.hoisted(() => vi.fn()); const mockSetDefaultPasswordActive = vi.fn(); const mockGetState = vi.fn(); @@ -13,6 +14,10 @@ vi.mock("@/protoOS/api/hooks/useRefresh", () => ({ useRefresh: () => mockRefresh, })); +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: mockUseMinerHosting, +})); + vi.mock("../useMinerStore", () => ({ default: Object.assign( vi.fn((selector: any) => @@ -55,6 +60,7 @@ describe("useAuthErrors", () => { vi.clearAllMocks(); __resetRefreshInFlightForTest(); mockUseLocation.mockReturnValue({ pathname: "/" }); + mockUseMinerHosting.mockReturnValue({ mode: "direct" }); mockGetState.mockReturnValue({ auth: { authTokens: { @@ -92,6 +98,25 @@ describe("useAuthErrors", () => { expect(onSuccess).toHaveBeenCalledWith("new-token"); }); + test("does not run direct miner refresh or show the login modal for fleet-hosted 401s", () => { + mockUseMinerHosting.mockReturnValue({ mode: "fleet" }); + const onError = vi.fn(); + + const { result } = renderHook(() => useAuthErrors()); + + const error = { status: 401, error: { message: "Unauthorized" } }; + const returnValue = result.current.handleAuthErrors({ + error, + onError, + }); + + expect(returnValue).toBeUndefined(); + expect(mockRefresh).not.toHaveBeenCalled(); + expect(mockLogout).not.toHaveBeenCalled(); + expect(mockSetShowLoginModal).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalledWith(error); + }); + test("uses the latest refresh token from store when handling 401s", () => { mockGetState.mockReturnValue({ auth: { diff --git a/client/src/protoOS/store/hooks/useAuth.ts b/client/src/protoOS/store/hooks/useAuth.ts index be73e0f7e..01d2afb04 100644 --- a/client/src/protoOS/store/hooks/useAuth.ts +++ b/client/src/protoOS/store/hooks/useAuth.ts @@ -4,6 +4,7 @@ import useMinerStore from "../useMinerStore"; import { ErrorProps } from "@/protoOS/api/apiResponseTypes"; import { isDefaultPasswordActiveError } from "@/protoOS/api/defaultPasswordContract"; import { useRefresh } from "@/protoOS/api/hooks/useRefresh"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; import { isAuthRequiredPath } from "@/protoOS/routeAuth"; // ============================================================================= @@ -70,9 +71,16 @@ export const useAuthErrors = () => { const setShowLoginModal = useMinerStore((state) => state.ui.setShowLoginModal); const setDefaultPasswordActive = useMinerStore((state) => state.minerStatus.setDefaultPasswordActive); const refresh = useRefresh(); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; const handleAuthErrors = useCallback( ({ error, onError, onSuccess }: HandleAuthErrorsProps) => { + if (isFleetHosted) { + onError?.(error); + return; + } + // 403 with DEFAULT_PASSWORD_ACTIVE means the device still has its factory // password. Surface this in the store so the UI can prompt a password change. if (isDefaultPasswordActiveError(error)) { @@ -111,7 +119,7 @@ export const useAuthErrors = () => { } onError?.(error); }, - [refresh, logout, setShowLoginModal, setDefaultPasswordActive], + [isFleetHosted, refresh, logout, setShowLoginModal, setDefaultPasswordActive], ); return useMemo( diff --git a/client/src/protoOS/store/hooks/useAuthRetry.test.ts b/client/src/protoOS/store/hooks/useAuthRetry.test.ts index e79d427a7..319cbd1fd 100644 --- a/client/src/protoOS/store/hooks/useAuthRetry.test.ts +++ b/client/src/protoOS/store/hooks/useAuthRetry.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { useAuthRetry } from "./useAuthRetry"; const mockHandleAuthErrors = vi.fn(); +const mockUseMinerHosting = vi.hoisted(() => vi.fn()); const mockGetState = vi.fn(); let currentAccessToken = "test-token"; @@ -21,9 +22,14 @@ vi.mock("./useAuth", () => ({ useAuthErrors: vi.fn(() => ({ handleAuthErrors: mockHandleAuthErrors })), })); +vi.mock("@/protoOS/contexts/MinerHostingContext", () => ({ + useMinerHosting: mockUseMinerHosting, +})); + describe("useAuthRetry", () => { beforeEach(() => { vi.clearAllMocks(); + mockUseMinerHosting.mockReturnValue({ mode: "direct" }); currentAccessToken = "test-token"; mockGetState.mockImplementation(() => ({ auth: { @@ -69,6 +75,21 @@ describe("useAuthRetry", () => { }); }); + test("does not use direct miner bearer auth or refresh retry in fleet-hosted mode", async () => { + mockUseMinerHosting.mockReturnValue({ mode: "fleet" }); + const error = { status: 401, error: { message: "Unauthorized" } }; + const request = vi.fn().mockRejectedValue(error); + const onError = vi.fn(); + + const { result } = renderHook(() => useAuthRetry()); + + await result.current({ request, onError }); + + expect(request).toHaveBeenCalledWith({ secure: false }); + expect(onError).toHaveBeenCalledWith(error); + expect(mockHandleAuthErrors).not.toHaveBeenCalled(); + }); + test("retries with fresh token on successful refresh", async () => { const error = { status: 401, error: { message: "Unauthorized" } }; const request = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce("retry-result"); diff --git a/client/src/protoOS/store/hooks/useAuthRetry.ts b/client/src/protoOS/store/hooks/useAuthRetry.ts index 4537b692b..51abf2929 100644 --- a/client/src/protoOS/store/hooks/useAuthRetry.ts +++ b/client/src/protoOS/store/hooks/useAuthRetry.ts @@ -3,8 +3,9 @@ import useMinerStore from "../useMinerStore"; import { useAuthErrors } from "./useAuth"; import { ErrorProps } from "@/protoOS/api/apiResponseTypes"; import { RequestParams } from "@/protoOS/api/generatedApi"; +import { useMinerHosting } from "@/protoOS/contexts/MinerHostingContext"; -type AuthRequestParams = RequestParams & { headers: { Authorization: string } }; +type AuthRequestParams = RequestParams & { headers?: { Authorization?: string } }; interface AuthRetryOptions { request: (params: AuthRequestParams) => Promise; @@ -22,21 +23,25 @@ interface AuthRetryOptions { */ export const useAuthRetry = () => { const { handleAuthErrors } = useAuthErrors(); + const { mode } = useMinerHosting(); + const isFleetHosted = mode === "fleet"; return useCallback( ({ request, onSuccess, onError, shouldRetry }: AuthRetryOptions): Promise => { - const authRequestParams: AuthRequestParams = { - secure: false, - headers: { - Authorization: `Bearer ${useMinerStore.getState().auth.authTokens.accessToken?.value || ""}`, - }, - }; + const authRequestParams: AuthRequestParams = isFleetHosted + ? { secure: false } + : { + secure: false, + headers: { + Authorization: `Bearer ${useMinerStore.getState().auth.authTokens.accessToken?.value || ""}`, + }, + }; const execute = (params: AuthRequestParams, isRetry = false): Promise => request(params) .then((result) => onSuccess?.(result)) .catch((error) => { - if (isRetry || (shouldRetry && !shouldRetry(error))) { + if (isFleetHosted || isRetry || (shouldRetry && !shouldRetry(error))) { onError?.(error); return; } @@ -56,6 +61,6 @@ export const useAuthRetry = () => { return execute(authRequestParams); }, - [handleAuthErrors], + [handleAuthErrors, isFleetHosted], ); }; diff --git a/plugin/proto/go.mod b/plugin/proto/go.mod index 9087f5b06..77e9d65a7 100644 --- a/plugin/proto/go.mod +++ b/plugin/proto/go.mod @@ -4,7 +4,6 @@ go 1.25.4 require ( github.com/block/proto-fleet/server v0.0.0-20260612045813-75933fce45c2 - github.com/golang-jwt/jwt/v5 v5.3.1 github.com/hashicorp/go-plugin v1.8.0 github.com/moby/moby/api v1.54.2 github.com/moby/moby/client v0.4.1 diff --git a/plugin/proto/go.sum b/plugin/proto/go.sum index cbe76afcb..1a25d2458 100644 --- a/plugin/proto/go.sum +++ b/plugin/proto/go.sum @@ -49,8 +49,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/proto/fleetmanagement/v1/fleetmanagement.proto b/proto/fleetmanagement/v1/fleetmanagement.proto index 8fdb7a647..255eaaac9 100644 --- a/proto/fleetmanagement/v1/fleetmanagement.proto +++ b/proto/fleetmanagement/v1/fleetmanagement.proto @@ -155,6 +155,12 @@ message MinerStateSnapshot { // device is not assigned at that level; groups is empty when the device is // not a member of any group. common.v1.PlacementRefs placement = 28; + + // True when Fleet can open an embedded web management view for this miner. + // Availability is computed from driver support, pairing state, and routing + // ownership; unsupported or fleet-node-owned devices should use their external + // URL or typed control-stream commands instead. + bool embedded_web_view_available = 29; } // Removed MinerComponentStatus and ComponentStatus - now using error-based filtering diff --git a/server/cmd/fleetd/main.go b/server/cmd/fleetd/main.go index a23fdcee6..e4cba531d 100644 --- a/server/cmd/fleetd/main.go +++ b/server/cmd/fleetd/main.go @@ -108,6 +108,7 @@ import ( foremanImportHandler "github.com/block/proto-fleet/server/internal/handlers/foremanimport" "github.com/block/proto-fleet/server/internal/handlers/interceptors" "github.com/block/proto-fleet/server/internal/handlers/middleware" + minerProxyHandler "github.com/block/proto-fleet/server/internal/handlers/minerproxy" "github.com/block/proto-fleet/server/internal/handlers/networkinfo" notificationsHandler "github.com/block/proto-fleet/server/internal/handlers/notifications" "github.com/block/proto-fleet/server/internal/handlers/onboarding" @@ -588,6 +589,7 @@ func start(config *Config) error { mux.Handle("GET /api/v1/firmware/files", firmwareHandler.NewListFilesHandler(filesService, sessionSvc, userStore)) mux.Handle("DELETE /api/v1/firmware/files/{fileId}", firmwareHandler.NewDeleteFileHandler(filesService, sessionSvc, userStore)) mux.Handle("DELETE /api/v1/firmware/files", firmwareHandler.NewDeleteAllFilesHandler(filesService, sessionSvc, userStore)) + mux.Handle("/miners/{deviceIdentifier}/api/v1/{rest...}", minerProxyHandler.NewHandler(conn, sessionSvc, userStore, permissionResolver, encryptSvc)) chunkedCleanupCtx, chunkedCleanupCancel := context.WithCancel(context.Background()) go chunkedMgr.StartCleanup(chunkedCleanupCtx, config.Files.ChunkedUploadSessionTTL) diff --git a/server/generated/grpc/fleetmanagement/v1/fleetmanagement.pb.go b/server/generated/grpc/fleetmanagement/v1/fleetmanagement.pb.go index a73e96b98..07be5496b 100644 --- a/server/generated/grpc/fleetmanagement/v1/fleetmanagement.pb.go +++ b/server/generated/grpc/fleetmanagement/v1/fleetmanagement.pb.go @@ -572,9 +572,14 @@ type MinerStateSnapshot struct { // Placement references for this device. Individual refs are unset when the // device is not assigned at that level; groups is empty when the device is // not a member of any group. - Placement *v1.PlacementRefs `protobuf:"bytes,28,opt,name=placement,proto3" json:"placement,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Placement *v1.PlacementRefs `protobuf:"bytes,28,opt,name=placement,proto3" json:"placement,omitempty"` + // True when Fleet can open an embedded web management view for this miner. + // Availability is computed from driver support, pairing state, and routing + // ownership; unsupported or fleet-node-owned devices should use their external + // URL or typed control-stream commands instead. + EmbeddedWebViewAvailable bool `protobuf:"varint,29,opt,name=embedded_web_view_available,json=embeddedWebViewAvailable,proto3" json:"embedded_web_view_available,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MinerStateSnapshot) Reset() { @@ -761,6 +766,13 @@ func (x *MinerStateSnapshot) GetPlacement() *v1.PlacementRefs { return nil } +func (x *MinerStateSnapshot) GetEmbeddedWebViewAvailable() bool { + if x != nil { + return x.EmbeddedWebViewAvailable + } + return false +} + // Request to list miners with their metadata // Returns paginated list of miners with identification info and status // Use GetBatchMinerTelemetry to fetch telemetry for specific miners after initial load @@ -2948,7 +2960,7 @@ var file_fleetmanagement_v1_fleetmanagement_proto_rawDesc = string([]byte{ 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x7a, 0x6f, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0xfb, 0x08, 0x0a, 0x12, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x6f, 0x22, 0xba, 0x09, 0x0a, 0x12, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, @@ -3013,555 +3025,559 @@ var file_fleetmanagement_v1_fleetmanagement_proto_rawDesc = string([]byte{ 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x66, 0x73, 0x52, 0x09, 0x70, 0x6c, 0x61, - 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x10, 0x10, 0x11, 0x4a, 0x04, 0x08, 0x14, - 0x10, 0x15, 0x4a, 0x04, 0x08, 0x15, 0x10, 0x16, 0x4a, 0x04, 0x08, 0x19, 0x10, 0x1a, 0x4a, 0x04, - 0x08, 0x1a, 0x10, 0x1b, 0x4a, 0x04, 0x08, 0x1b, 0x10, 0x1c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x52, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x0a, - 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x07, 0x73, 0x69, 0x74, 0x65, - 0x5f, 0x69, 0x64, 0x52, 0x0a, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x52, - 0x0e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, - 0xc9, 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x27, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x1a, 0x05, 0x18, 0xe8, 0x07, 0x28, - 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, - 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, - 0x73, 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, - 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x12, 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xeb, 0x05, 0x0a, 0x0f, - 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, - 0x45, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x15, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, - 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x13, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x12, 0x4c, 0x0a, 0x10, - 0x70, 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, - 0x18, 0x06, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x69, 0x72, - 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0f, 0x70, 0x61, 0x69, 0x72, 0x69, - 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x03, 0x52, 0x08, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x5f, - 0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x49, - 0x64, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x66, - 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x18, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, - 0x18, 0x01, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, - 0x65, 0x72, 0x69, 0x63, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x52, 0x61, - 0x6e, 0x67, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0d, 0x6e, 0x75, 0x6d, 0x65, 0x72, - 0x69, 0x63, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x70, 0x5f, 0x63, - 0x69, 0x64, 0x72, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x70, 0x43, 0x69, - 0x64, 0x72, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, - 0x0d, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x73, 0x12, 0x2d, - 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, - 0x75, 0x64, 0x65, 0x55, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x21, 0x0a, - 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0f, 0x20, - 0x03, 0x28, 0x03, 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, - 0x12, 0x2e, 0x0a, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x6e, 0x6f, 0x5f, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, - 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x4e, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, - 0x12, 0x2f, 0x0a, 0x09, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x11, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x5a, 0x6f, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x7a, 0x6f, 0x6e, 0x65, 0x4b, 0x65, 0x79, - 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x6e, 0x6f, 0x5f, - 0x72, 0x61, 0x63, 0x6b, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, - 0x75, 0x64, 0x65, 0x4e, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a, 0x12, 0x4e, 0x75, - 0x6d, 0x65, 0x72, 0x69, 0x63, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x12, 0x36, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x20, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2e, 0x0a, 0x03, 0x6d, 0x69, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x03, 0x6d, 0x69, 0x6e, 0x12, 0x2e, 0x0a, 0x03, 0x6d, 0x61, 0x78, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x03, 0x6d, 0x61, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, - 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x6d, 0x69, 0x6e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, - 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, - 0x76, 0x65, 0x22, 0xaf, 0x02, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, - 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x21, - 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x73, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x10, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x72, 0x6d, 0x77, - 0x61, 0x72, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x10, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x22, 0xe7, 0x01, 0x0a, 0x15, - 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, - 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x52, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x06, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xab, 0x01, 0x0a, 0x19, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, - 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, - 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x12, 0x51, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, - 0x75, 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, - 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x73, 0x76, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, 0x6e, - 0x69, 0x74, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, - 0x6e, 0x69, 0x74, 0x22, 0x37, 0x0a, 0x1a, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x73, 0x76, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x73, 0x76, 0x44, 0x61, 0x74, 0x61, 0x22, 0x66, 0x0a, 0x1a, - 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, - 0x74, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x73, 0x69, - 0x74, 0x65, 0x49, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, - 0x5f, 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x55, 0x6e, 0x61, 0x73, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x22, 0x83, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6d, 0x69, - 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x41, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x0b, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x55, 0x0a, 0x1e, 0x47, 0x65, - 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x11, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, - 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x22, 0x68, 0x0a, 0x0e, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, - 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x88, 0x01, - 0x01, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x42, - 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x22, 0x5b, 0x0a, 0x1f, 0x47, - 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, - 0x0a, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, - 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x22, 0x61, 0x0a, 0x0f, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, - 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, - 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x59, 0x0a, 0x1a, 0x47, - 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, - 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x5a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x1b, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x64, + 0x65, 0x64, 0x5f, 0x77, 0x65, 0x62, 0x5f, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x61, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x65, 0x6d, 0x62, + 0x65, 0x64, 0x64, 0x65, 0x64, 0x57, 0x65, 0x62, 0x56, 0x69, 0x65, 0x77, 0x41, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4a, 0x04, 0x08, 0x10, 0x10, 0x11, 0x4a, 0x04, 0x08, 0x14, 0x10, + 0x15, 0x4a, 0x04, 0x08, 0x15, 0x10, 0x16, 0x4a, 0x04, 0x08, 0x19, 0x10, 0x1a, 0x4a, 0x04, 0x08, + 0x1a, 0x10, 0x1b, 0x4a, 0x04, 0x08, 0x1b, 0x10, 0x1c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x52, + 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x0a, 0x72, + 0x61, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x07, 0x73, 0x69, 0x74, 0x65, 0x5f, + 0x69, 0x64, 0x52, 0x0a, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x52, 0x0e, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0xc9, + 0x01, 0x0a, 0x1e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x27, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x1a, 0x05, 0x18, 0xe8, 0x07, 0x28, 0x00, + 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x75, + 0x72, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, 0x73, + 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, + 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0xeb, 0x05, 0x0a, 0x0f, 0x4d, + 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x45, + 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x15, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x13, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x12, 0x4c, 0x0a, 0x10, 0x70, + 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, + 0x06, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x69, 0x72, 0x69, + 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0f, 0x70, 0x61, 0x69, 0x72, 0x69, 0x6e, + 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x03, 0x52, 0x08, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, + 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x64, + 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x66, 0x69, + 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, + 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x6e, 0x75, 0x6d, 0x65, + 0x72, 0x69, 0x63, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0d, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, + 0x63, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x70, 0x5f, 0x63, 0x69, + 0x64, 0x72, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x70, 0x43, 0x69, 0x64, + 0x72, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0d, + 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x73, 0x12, 0x2d, 0x0a, + 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x55, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0f, 0x20, 0x03, + 0x28, 0x03, 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, + 0x2e, 0x0a, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x6e, 0x6f, 0x5f, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x69, 0x6e, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x4e, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, + 0x2f, 0x0a, 0x09, 0x7a, 0x6f, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x11, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x5a, + 0x6f, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x7a, 0x6f, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x6e, 0x6f, 0x5f, 0x72, + 0x61, 0x63, 0x6b, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x4e, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a, 0x12, 0x4e, 0x75, 0x6d, + 0x65, 0x72, 0x69, 0x63, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, + 0x36, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, + 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x46, 0x69, 0x65, 0x6c, 0x64, + 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2e, 0x0a, 0x03, 0x6d, 0x69, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x03, 0x6d, 0x69, 0x6e, 0x12, 0x2e, 0x0a, 0x03, 0x6d, 0x61, 0x78, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x03, 0x6d, 0x61, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, + 0x6d, 0x69, 0x6e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, + 0x65, 0x22, 0xaf, 0x02, 0x0a, 0x1f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x22, 0x51, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, - 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x33, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, - 0xc8, 0x01, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x58, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x63, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x5f, - 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, - 0x64, 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x22, - 0xb6, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x12, 0x46, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x06, 0x6d, + 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x21, 0x0a, + 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x10, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, + 0x72, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x10, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x22, 0xe7, 0x01, 0x0a, 0x15, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x48, 0x00, 0x52, 0x0a, - 0x61, 0x6c, 0x6c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x0f, 0x69, 0x6e, - 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x42, 0x10, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x62, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x4b, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0e, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x3b, 0x0a, 0x14, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xe3, 0x01, 0x0a, 0x13, 0x52, 0x65, - 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, + 0x09, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x06, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, - 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, - 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, - 0x87, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6e, 0x61, - 0x6d, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0c, 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, - 0x0f, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x66, 0x61, - 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc4, 0x02, 0x0a, 0x18, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x0b, 0x6e, - 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x6e, - 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, - 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, - 0x73, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, - 0x72, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x22, 0xb7, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, - 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x75, 0x6e, - 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, - 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x29, 0x0a, 0x10, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x74, 0x63, 0x68, - 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4a, - 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, - 0x65, 0x72, 0x74, 0x79, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0a, - 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x65, - 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xba, - 0x48, 0x0d, 0x72, 0x0b, 0x52, 0x01, 0x2d, 0x52, 0x01, 0x5f, 0x52, 0x01, 0x2e, 0x52, 0x00, 0x52, - 0x09, 0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x90, 0x03, 0x0a, 0x0c, 0x4e, - 0x61, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x5c, 0x0a, 0x12, 0x73, - 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x41, 0x6e, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x10, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x41, - 0x6e, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, + 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xab, 0x01, 0x0a, 0x19, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, + 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, + 0x51, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x75, + 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x73, 0x76, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, 0x6e, 0x69, + 0x74, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, 0x6e, + 0x69, 0x74, 0x22, 0x37, 0x0a, 0x1a, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x19, 0x0a, 0x08, 0x63, 0x73, 0x76, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x07, 0x63, 0x73, 0x76, 0x44, 0x61, 0x74, 0x61, 0x22, 0x66, 0x0a, 0x1a, 0x47, + 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x73, 0x69, 0x74, + 0x65, 0x49, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, + 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x55, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x22, 0x83, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6d, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x41, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x0b, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x55, 0x0a, 0x1e, 0x47, 0x65, 0x74, + 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x11, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x10, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x22, 0x68, 0x0a, 0x0e, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, + 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x88, 0x01, 0x01, + 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, + 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0a, + 0x0a, 0x08, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x22, 0x5b, 0x0a, 0x1f, 0x47, 0x65, + 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, + 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, + 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x22, 0x61, 0x0a, 0x0f, 0x4d, 0x69, 0x6e, 0x65, 0x72, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x59, 0x0a, 0x1a, 0x47, 0x65, + 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x5a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, + 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x73, 0x22, 0x51, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6f, + 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x33, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, + 0x01, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x22, 0x58, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, + 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x63, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, + 0x65, 0x52, 0x0b, 0x63, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0xb6, + 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x12, 0x46, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x48, 0x00, 0x52, 0x0a, 0x61, + 0x6c, 0x6c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x0f, 0x69, 0x6e, 0x63, + 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x4c, + 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x42, 0x10, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x62, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, + 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x3b, 0x0a, 0x14, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xe3, 0x01, 0x0a, 0x13, 0x52, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, + 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, - 0x00, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x73, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, - 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, - 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, - 0x48, 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, - 0x0a, 0x09, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, 0x71, 0x75, 0x61, 0x6c, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0xa8, 0x01, - 0x0a, 0x18, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x41, 0x6e, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, - 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6f, + 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x22, 0x87, + 0x01, 0x0a, 0x14, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6e, 0x61, 0x6d, + 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, + 0x72, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, + 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x66, 0x61, 0x69, + 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc4, 0x02, 0x0a, 0x18, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, + 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x0b, 0x6e, 0x61, + 0x6d, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x6e, 0x61, + 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x73, + 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, + 0x02, 0x10, 0x01, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x2c, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, + 0xb7, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x75, 0x6e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x75, 0x6e, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, + 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, + 0x0a, 0x10, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4a, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x79, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x08, 0x01, 0x52, 0x0a, 0x70, + 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x73, 0x65, 0x70, + 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xba, 0x48, + 0x0d, 0x72, 0x0b, 0x52, 0x01, 0x2d, 0x52, 0x01, 0x5f, 0x52, 0x01, 0x2e, 0x52, 0x00, 0x52, 0x09, + 0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x90, 0x03, 0x0a, 0x0c, 0x4e, 0x61, + 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x5c, 0x0a, 0x12, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x41, 0x6e, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, + 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x10, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x41, 0x6e, + 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x3f, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, + 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x73, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x78, + 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, + 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, + 0x09, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x48, 0x00, 0x52, 0x09, 0x71, 0x75, 0x61, 0x6c, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0xa8, 0x01, 0x0a, + 0x18, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x41, 0x6e, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, + 0xba, 0x48, 0x06, 0x1a, 0x04, 0x18, 0x06, 0x28, 0x01, 0x52, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x22, 0x6f, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, + 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xba, 0x48, 0x06, 0x1a, 0x04, 0x18, 0x06, 0x28, 0x01, 0x52, 0x0c, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x65, 0x72, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x22, 0x6f, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x2c, 0x0a, 0x0d, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x0d, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x42, 0x09, 0xba, 0x48, 0x06, 0x1a, 0x04, 0x18, 0x06, 0x28, 0x01, 0x52, 0x0c, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x65, 0x72, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x22, 0x2f, 0x0a, 0x0e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x91, 0x03, 0x0a, 0x12, 0x46, - 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, - 0x79, 0x12, 0x40, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x22, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x37, 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xba, 0x48, - 0x06, 0x1a, 0x04, 0x18, 0x06, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x07, - 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, - 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x48, 0x01, 0x52, - 0x07, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x3a, 0x90, 0x01, 0xba, 0x48, - 0x8c, 0x01, 0x1a, 0x89, 0x01, 0x0a, 0x25, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, - 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x63, 0x68, 0x61, - 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2f, 0x73, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x2f, 0x21, - 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, - 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, - 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x42, 0x12, - 0x0a, 0x10, 0x5f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x84, - 0x01, 0x0a, 0x11, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, - 0x65, 0x72, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x75, 0x66, 0x66, 0x69, 0x78, 0x2a, 0x99, 0x01, 0x0a, 0x1f, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a, 0x2f, 0x46, 0x4c, 0x45, - 0x45, 0x54, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x45, - 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x41, - 0x0a, 0x3d, 0x46, 0x4c, 0x45, 0x45, 0x54, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, - 0x4e, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x50, 0x41, - 0x47, 0x49, 0x4e, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x55, 0x52, 0x53, 0x4f, 0x52, 0x10, - 0x01, 0x2a, 0x9a, 0x02, 0x0a, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x44, - 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, - 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x41, - 0x4e, 0x43, 0x45, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1a, - 0x0a, 0x16, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x49, 0x4e, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, - 0x53, 0x5f, 0x4d, 0x49, 0x4e, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4f, 0x4f, 0x4c, 0x10, 0x06, 0x12, - 0x1a, 0x0a, 0x16, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x21, 0x0a, 0x1d, 0x44, - 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x42, - 0x4f, 0x4f, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, 0x08, 0x2a, 0xed, - 0x01, 0x0a, 0x0d, 0x50, 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x1e, 0x0a, 0x1a, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x19, 0x0a, 0x15, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x50, 0x41, 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x50, - 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, - 0x50, 0x41, 0x49, 0x52, 0x45, 0x44, 0x10, 0x02, 0x12, 0x28, 0x0a, 0x24, 0x50, 0x41, 0x49, 0x52, - 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, - 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x45, 0x44, - 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x19, - 0x0a, 0x15, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x41, 0x49, - 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x45, 0x46, 0x41, - 0x55, 0x4c, 0x54, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x06, 0x2a, 0xe6, - 0x01, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, - 0x1d, 0x0a, 0x19, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, - 0x0a, 0x1a, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, - 0x48, 0x41, 0x53, 0x48, 0x52, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x53, 0x10, 0x01, 0x12, 0x20, - 0x0a, 0x1c, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, - 0x45, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x4a, 0x54, 0x48, 0x10, 0x02, - 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, - 0x44, 0x5f, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x4b, 0x57, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, - 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x54, 0x45, - 0x4d, 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x43, 0x10, 0x04, 0x12, 0x1b, 0x0a, - 0x17, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x56, - 0x4f, 0x4c, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x56, 0x10, 0x05, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x55, - 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x43, 0x55, 0x52, 0x52, - 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x10, 0x06, 0x2a, 0x81, 0x01, 0x0a, 0x12, 0x43, 0x73, 0x76, 0x54, - 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x24, - 0x0a, 0x20, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, - 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, 0x4d, 0x50, - 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x43, 0x45, 0x4c, - 0x53, 0x49, 0x55, 0x53, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, - 0x4d, 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x46, - 0x41, 0x48, 0x52, 0x45, 0x4e, 0x48, 0x45, 0x49, 0x54, 0x10, 0x02, 0x2a, 0x99, 0x02, 0x0a, 0x0e, - 0x46, 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, - 0x0a, 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x5f, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, - 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, - 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x4e, 0x55, - 0x4d, 0x42, 0x45, 0x52, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, - 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x4f, 0x52, 0x4b, 0x45, - 0x52, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x49, 0x58, 0x45, - 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x4f, 0x44, - 0x45, 0x4c, 0x10, 0x04, 0x12, 0x21, 0x0a, 0x1d, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, - 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, 0x46, 0x41, 0x43, - 0x54, 0x55, 0x52, 0x45, 0x52, 0x10, 0x05, 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x49, 0x58, 0x45, 0x44, - 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x4f, 0x43, 0x41, - 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x06, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, - 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, - 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x07, 0x2a, 0x6e, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x1d, 0x43, - 0x48, 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, - 0x0a, 0x17, 0x43, 0x48, 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x52, 0x53, 0x54, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x43, - 0x48, 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x4c, 0x41, 0x53, 0x54, 0x10, 0x02, 0x2a, 0x87, 0x01, 0x0a, 0x0d, 0x51, 0x75, 0x61, 0x6c, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x51, 0x55, 0x41, - 0x4c, 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x51, 0x55, 0x41, - 0x4c, 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x55, 0x49, 0x4c, - 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, - 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x10, 0x02, 0x12, - 0x20, 0x0a, 0x1c, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, - 0x03, 0x32, 0x9f, 0x09, 0x0a, 0x16, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x82, 0x01, 0x0a, - 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x66, - 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x64, 0x0a, 0x0d, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x73, 0x12, 0x28, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x66, + 0x74, 0x65, 0x72, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x22, 0x2f, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, + 0x10, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x91, 0x03, 0x0a, 0x12, 0x46, 0x69, + 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, + 0x12, 0x40, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, + 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x37, 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xba, 0x48, 0x06, + 0x1a, 0x04, 0x18, 0x06, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, + 0x74, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x07, 0x73, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x12, 0x45, 0x78, 0x70, 0x6f, 0x72, - 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x12, 0x2d, 0x2e, - 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, - 0x73, 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x66, + 0x31, 0x2e, 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x48, 0x01, 0x52, 0x07, + 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x3a, 0x90, 0x01, 0xba, 0x48, 0x8c, + 0x01, 0x1a, 0x89, 0x01, 0x0a, 0x25, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x63, 0x68, 0x61, 0x72, + 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2f, 0x73, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x69, 0x73, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x2f, 0x21, 0x68, + 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, + 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x68, 0x61, 0x73, 0x28, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x29, 0x42, 0x12, 0x0a, + 0x10, 0x5f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x84, 0x01, + 0x0a, 0x11, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, + 0x66, 0x66, 0x69, 0x78, 0x2a, 0x99, 0x01, 0x0a, 0x1f, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a, 0x2f, 0x46, 0x4c, 0x45, 0x45, + 0x54, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x45, 0x52, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x41, 0x0a, + 0x3d, 0x46, 0x4c, 0x45, 0x45, 0x54, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, + 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x50, 0x41, 0x47, + 0x49, 0x4e, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x55, 0x52, 0x53, 0x4f, 0x52, 0x10, 0x01, + 0x2a, 0x9a, 0x02, 0x0a, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x18, 0x0a, 0x14, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x44, 0x45, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, + 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x41, 0x4e, + 0x43, 0x45, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x1a, 0x0a, + 0x16, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, + 0x4e, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x56, + 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x53, + 0x5f, 0x4d, 0x49, 0x4e, 0x49, 0x4e, 0x47, 0x5f, 0x50, 0x4f, 0x4f, 0x4c, 0x10, 0x06, 0x12, 0x1a, + 0x0a, 0x16, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x55, 0x50, 0x44, 0x41, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x42, 0x4f, + 0x4f, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, 0x08, 0x2a, 0xed, 0x01, + 0x0a, 0x0d, 0x50, 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x1e, 0x0a, 0x1a, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x19, 0x0a, 0x15, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x50, 0x41, 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x41, + 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x50, + 0x41, 0x49, 0x52, 0x45, 0x44, 0x10, 0x02, 0x12, 0x28, 0x0a, 0x24, 0x50, 0x41, 0x49, 0x52, 0x49, + 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, + 0x54, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, + 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x19, 0x0a, + 0x15, 0x50, 0x41, 0x49, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x41, 0x49, 0x52, + 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, + 0x4c, 0x54, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x06, 0x2a, 0xe6, 0x01, + 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1d, + 0x0a, 0x19, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, + 0x1a, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x48, + 0x41, 0x53, 0x48, 0x52, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x53, 0x10, 0x01, 0x12, 0x20, 0x0a, + 0x1c, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x45, + 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x4a, 0x54, 0x48, 0x10, 0x02, 0x12, + 0x1a, 0x0a, 0x16, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, + 0x5f, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x4b, 0x57, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x4e, + 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x54, 0x45, 0x4d, + 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x43, 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, + 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x56, 0x4f, + 0x4c, 0x54, 0x41, 0x47, 0x45, 0x5f, 0x56, 0x10, 0x05, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x55, 0x4d, + 0x45, 0x52, 0x49, 0x43, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x43, 0x55, 0x52, 0x52, 0x45, + 0x4e, 0x54, 0x5f, 0x41, 0x10, 0x06, 0x2a, 0x81, 0x01, 0x0a, 0x12, 0x43, 0x73, 0x76, 0x54, 0x65, + 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x24, 0x0a, + 0x20, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, + 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x45, + 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x43, 0x45, 0x4c, 0x53, + 0x49, 0x55, 0x53, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x53, 0x56, 0x5f, 0x54, 0x45, 0x4d, + 0x50, 0x45, 0x52, 0x41, 0x54, 0x55, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, 0x46, 0x41, + 0x48, 0x52, 0x45, 0x4e, 0x48, 0x45, 0x49, 0x54, 0x10, 0x02, 0x2a, 0x99, 0x02, 0x0a, 0x0e, 0x46, + 0x69, 0x78, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, + 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x20, 0x0a, 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x43, 0x5f, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, 0x10, + 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x4e, 0x55, 0x4d, + 0x42, 0x45, 0x52, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, + 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x4f, 0x52, 0x4b, 0x45, 0x52, + 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x49, 0x58, 0x45, 0x44, + 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, + 0x4c, 0x10, 0x04, 0x12, 0x21, 0x0a, 0x1d, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, 0x41, 0x4c, + 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, 0x46, 0x41, 0x43, 0x54, + 0x55, 0x52, 0x45, 0x52, 0x10, 0x05, 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, + 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x4f, 0x43, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x10, 0x06, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x49, 0x58, 0x45, 0x44, 0x5f, 0x56, + 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, + 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x07, 0x2a, 0x6e, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, + 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x48, + 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, + 0x17, 0x43, 0x48, 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x52, 0x53, 0x54, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x48, + 0x41, 0x52, 0x41, 0x43, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x4c, 0x41, 0x53, 0x54, 0x10, 0x02, 0x2a, 0x87, 0x01, 0x0a, 0x0d, 0x51, 0x75, 0x61, 0x6c, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x51, 0x55, 0x41, 0x4c, + 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x51, 0x55, 0x41, 0x4c, + 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, + 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x49, + 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x10, 0x02, 0x12, 0x20, + 0x0a, 0x1c, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x49, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, + 0x32, 0x9f, 0x09, 0x0a, 0x16, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, + 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x66, 0x6c, + 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x64, 0x0a, 0x0d, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, + 0x73, 0x12, 0x28, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x66, 0x6c, + 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x12, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, + 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x73, 0x76, 0x12, 0x2d, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, - 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x76, - 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, - 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, + 0x74, 0x43, 0x73, 0x76, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x66, 0x6c, + 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x73, 0x76, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x76, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, + 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, + 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, - 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x47, - 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, - 0x64, 0x65, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x66, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x47, 0x65, + 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, + 0x65, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, + 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, + 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x61, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x66, 0x6c, + 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, + 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, - 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x2e, 0x2e, - 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x66, + 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, + 0x0c, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x6c, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, - 0x0a, 0x0c, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x27, - 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, - 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x70, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0xf0, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x6c, 0x65, 0x65, - 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x14, - 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, - 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, - 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x66, 0x6c, 0x65, 0x65, 0x74, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x76, 0x31, 0xa2, - 0x02, 0x03, 0x46, 0x58, 0x58, 0xaa, 0x02, 0x12, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x12, 0x46, 0x6c, 0x65, - 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0xe2, - 0x02, 0x1e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x13, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6e, 0x61, + 0x6d, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x70, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0xf0, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x14, 0x46, + 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, + 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, + 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x76, 0x31, 0xa2, 0x02, + 0x03, 0x46, 0x58, 0x58, 0xaa, 0x02, 0x12, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x12, 0x46, 0x6c, 0x65, 0x65, + 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, + 0x1e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x13, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/server/generated/sqlc/db.go b/server/generated/sqlc/db.go index 948ba8b95..77d1368ad 100644 --- a/server/generated/sqlc/db.go +++ b/server/generated/sqlc/db.go @@ -468,6 +468,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getDeviceWithCredentialsAndIPByDeviceIdentifierStmt, err = db.PrepareContext(ctx, getDeviceWithCredentialsAndIPByDeviceIdentifier); err != nil { return nil, fmt.Errorf("error preparing query GetDeviceWithCredentialsAndIPByDeviceIdentifier: %w", err) } + if q.getDirectProtoMinerProxyTargetStmt, err = db.PrepareContext(ctx, getDirectProtoMinerProxyTarget); err != nil { + return nil, fmt.Errorf("error preparing query GetDirectProtoMinerProxyTarget: %w", err) + } if q.getDiscoveredDeviceByDeviceIdentifierStmt, err = db.PrepareContext(ctx, getDiscoveredDeviceByDeviceIdentifier); err != nil { return nil, fmt.Errorf("error preparing query GetDiscoveredDeviceByDeviceIdentifier: %w", err) } @@ -2044,6 +2047,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getDeviceWithCredentialsAndIPByDeviceIdentifierStmt: %w", cerr) } } + if q.getDirectProtoMinerProxyTargetStmt != nil { + if cerr := q.getDirectProtoMinerProxyTargetStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getDirectProtoMinerProxyTargetStmt: %w", cerr) + } + } if q.getDiscoveredDeviceByDeviceIdentifierStmt != nil { if cerr := q.getDiscoveredDeviceByDeviceIdentifierStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getDiscoveredDeviceByDeviceIdentifierStmt: %w", cerr) @@ -3616,6 +3624,7 @@ type Queries struct { getDeviceStatusForDeviceIdentifiersStmt *sql.Stmt getDeviceStatusHourlyAggregatesStmt *sql.Stmt getDeviceWithCredentialsAndIPByDeviceIdentifierStmt *sql.Stmt + getDirectProtoMinerProxyTargetStmt *sql.Stmt getDiscoveredDeviceByDeviceIdentifierStmt *sql.Stmt getDiscoveredDeviceByIDStmt *sql.Stmt getDiscoveredDeviceByIPAndPortStmt *sql.Stmt @@ -4047,6 +4056,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getDeviceStatusForDeviceIdentifiersStmt: q.getDeviceStatusForDeviceIdentifiersStmt, getDeviceStatusHourlyAggregatesStmt: q.getDeviceStatusHourlyAggregatesStmt, getDeviceWithCredentialsAndIPByDeviceIdentifierStmt: q.getDeviceWithCredentialsAndIPByDeviceIdentifierStmt, + getDirectProtoMinerProxyTargetStmt: q.getDirectProtoMinerProxyTargetStmt, getDiscoveredDeviceByDeviceIdentifierStmt: q.getDiscoveredDeviceByDeviceIdentifierStmt, getDiscoveredDeviceByIDStmt: q.getDiscoveredDeviceByIDStmt, getDiscoveredDeviceByIPAndPortStmt: q.getDiscoveredDeviceByIPAndPortStmt, diff --git a/server/generated/sqlc/device.sql.go b/server/generated/sqlc/device.sql.go index 5f71169b3..da6e9b582 100644 --- a/server/generated/sqlc/device.sql.go +++ b/server/generated/sqlc/device.sql.go @@ -1789,7 +1789,8 @@ SELECT d.site_id, COALESCE(s.name, '') as site_label, d.building_id, - COALESCE(b.name, '') as building_label + COALESCE(b.name, '') as building_label, + FALSE as embedded_web_view_available FROM discovered_device dd LEFT JOIN device d ON dd.id = d.discovered_device_id LEFT JOIN device_pairing dp ON d.id = dp.device_id @@ -1800,28 +1801,29 @@ WHERE FALSE ` type ListMinerStateSnapshotsRow struct { - DeviceIdentifier string - MacAddress string - SerialNumber sql.NullString - Model sql.NullString - Manufacturer sql.NullString - FirmwareVersion sql.NullString - WorkerName sql.NullString - DeviceStatus NullDeviceStatusEnum - StatusTimestamp sql.NullTime - StatusDetails sql.NullString - IpAddress string - Port string - UrlScheme string - PairingStatus string - CursorID int64 - DeviceID int64 - DriverName string - CustomName sql.NullString - SiteID sql.NullInt64 - SiteLabel string - BuildingID sql.NullInt64 - BuildingLabel string + DeviceIdentifier string + MacAddress string + SerialNumber sql.NullString + Model sql.NullString + Manufacturer sql.NullString + FirmwareVersion sql.NullString + WorkerName sql.NullString + DeviceStatus NullDeviceStatusEnum + StatusTimestamp sql.NullTime + StatusDetails sql.NullString + IpAddress string + Port string + UrlScheme string + PairingStatus string + CursorID int64 + DeviceID int64 + DriverName string + CustomName sql.NullString + SiteID sql.NullInt64 + SiteLabel string + BuildingID sql.NullInt64 + BuildingLabel string + EmbeddedWebViewAvailable bool } // TYPE GENERATION STUB - This query is never executed. @@ -1860,6 +1862,7 @@ func (q *Queries) ListMinerStateSnapshots(ctx context.Context) ([]ListMinerState &i.SiteLabel, &i.BuildingID, &i.BuildingLabel, + &i.EmbeddedWebViewAvailable, ); err != nil { return nil, err } diff --git a/server/generated/sqlc/miner_service.sql.go b/server/generated/sqlc/miner_service.sql.go index 8ab595669..3a116ccf0 100644 --- a/server/generated/sqlc/miner_service.sql.go +++ b/server/generated/sqlc/miner_service.sql.go @@ -142,3 +142,75 @@ func (q *Queries) GetDeviceWithCredentialsAndIPByDeviceIdentifier(ctx context.Co ) return i, err } + +const getDirectProtoMinerProxyTarget = `-- name: GetDirectProtoMinerProxyTarget :one +SELECT + d.id, + d.device_identifier, + d.org_id, + d.site_id, + d.serial_number, + d.mac_address, + dd.ip_address, + dd.port, + dd.url_scheme, + dd.driver_name, + mc.username_enc, + mc.password_enc +FROM device d +JOIN discovered_device dd ON d.discovered_device_id = dd.id +JOIN device_pairing dp ON d.id = dp.device_id +LEFT JOIN miner_credentials mc ON d.id = mc.device_id +WHERE d.device_identifier = $1 + AND d.org_id = $2 + AND d.deleted_at IS NULL + AND dp.pairing_status IN ('PAIRED', 'DEFAULT_PASSWORD') + AND dd.driver_name = 'proto' + -- This HTTP proxy dials the miner from fleet-api. Fleet-node-owned + -- devices need a result-capable typed control command instead. + AND NOT EXISTS ( + SELECT 1 FROM fleet_node_device fnd + WHERE fnd.device_id = d.id AND fnd.org_id = d.org_id + ) +LIMIT 1 +` + +type GetDirectProtoMinerProxyTargetParams struct { + DeviceIdentifier string + OrgID int64 +} + +type GetDirectProtoMinerProxyTargetRow struct { + ID int64 + DeviceIdentifier string + OrgID int64 + SiteID sql.NullInt64 + SerialNumber sql.NullString + MacAddress string + IpAddress string + Port string + UrlScheme string + DriverName string + UsernameEnc sql.NullString + PasswordEnc sql.NullString +} + +func (q *Queries) GetDirectProtoMinerProxyTarget(ctx context.Context, arg GetDirectProtoMinerProxyTargetParams) (GetDirectProtoMinerProxyTargetRow, error) { + row := q.queryRow(ctx, q.getDirectProtoMinerProxyTargetStmt, getDirectProtoMinerProxyTarget, arg.DeviceIdentifier, arg.OrgID) + var i GetDirectProtoMinerProxyTargetRow + err := row.Scan( + &i.ID, + &i.DeviceIdentifier, + &i.OrgID, + &i.SiteID, + &i.SerialNumber, + &i.MacAddress, + &i.IpAddress, + &i.Port, + &i.UrlScheme, + &i.DriverName, + &i.UsernameEnc, + &i.PasswordEnc, + ) + return i, err +} diff --git a/server/internal/domain/fleetmanagement/service.go b/server/internal/domain/fleetmanagement/service.go index ceb9c2e0d..287b915fe 100644 --- a/server/internal/domain/fleetmanagement/service.go +++ b/server/internal/domain/fleetmanagement/service.go @@ -701,8 +701,9 @@ func (s *Service) buildSnapshotsFromUnifiedQuery( snapshots := make([]*pb.MinerStateSnapshot, 0, len(rows)) for _, row := range rows { snapshot := &pb.MinerStateSnapshot{ - DeviceIdentifier: row.DeviceIdentifier, - DriverName: row.DriverName, + DeviceIdentifier: row.DeviceIdentifier, + DriverName: row.DriverName, + EmbeddedWebViewAvailable: row.EmbeddedWebViewAvailable, } if row.SiteID.Valid { diff --git a/server/internal/domain/stores/sqlstores/device.go b/server/internal/domain/stores/sqlstores/device.go index 11710a98c..5a7ea4907 100644 --- a/server/internal/domain/stores/sqlstores/device.go +++ b/server/internal/domain/stores/sqlstores/device.go @@ -1094,6 +1094,7 @@ func (s *SQLDeviceStore) executeListQuery(ctx context.Context, orgID int64, curs &row.SiteLabel, &row.BuildingID, &row.BuildingLabel, + &row.EmbeddedWebViewAvailable, &row.SortValue, ) if err != nil { diff --git a/server/internal/domain/stores/sqlstores/device_integration_test.go b/server/internal/domain/stores/sqlstores/device_integration_test.go index b909710f0..dba1c4912 100644 --- a/server/internal/domain/stores/sqlstores/device_integration_test.go +++ b/server/internal/domain/stores/sqlstores/device_integration_test.go @@ -734,6 +734,94 @@ func TestCountMinersByState_FilterConsistency(t *testing.T) { require.False(t, identifiers["device-002"], "should NOT include INACTIVE device with error") } +func TestListMinerStateSnapshots_EmbeddedWebViewAvailable(t *testing.T) { + if testing.Short() { + t.Skip("Skipping database integration test in short mode") + } + + conn := testutil.GetTestDB(t) + ctx := t.Context() + store := sqlstores.NewSQLDeviceStore(conn) + + _, err := conn.Exec(` + INSERT INTO organization (id, org_id, name) + VALUES (1, '00000000-0000-0000-0000-000000000001', 'Test Org') + `) + require.NoError(t, err) + + fixtures := []struct { + id int64 + identifier string + driverName string + pairingStatus string + fleetNodeOwned bool + wantAvailable bool + }{ + {id: 9101, identifier: "direct-paired-proto", driverName: "proto", pairingStatus: "PAIRED", wantAvailable: true}, + {id: 9102, identifier: "direct-default-proto", driverName: "proto", pairingStatus: "DEFAULT_PASSWORD", wantAvailable: true}, + {id: 9103, identifier: "direct-auth-proto", driverName: "proto", pairingStatus: "AUTHENTICATION_NEEDED"}, + {id: 9104, identifier: "direct-paired-ant", driverName: "antminer", pairingStatus: "PAIRED"}, + {id: 9105, identifier: "discovered-only-proto", driverName: "proto"}, + {id: 9106, identifier: "fleet-node-proto", driverName: "proto", pairingStatus: "PAIRED", fleetNodeOwned: true}, + } + + var fleetNodeID int64 + require.NoError(t, conn.QueryRow(` + INSERT INTO fleet_node (org_id, name, identity_pubkey, enrollment_status) + VALUES (1, $1, $2, 'CONFIRMED') + RETURNING id + `, "web-view-node", []byte("web-view-node-key")).Scan(&fleetNodeID)) + + identifiers := make([]string, 0, len(fixtures)) + for i, fixture := range fixtures { + identifiers = append(identifiers, fixture.identifier) + _, err := conn.Exec(` + INSERT INTO discovered_device (id, org_id, device_identifier, model, manufacturer, driver_name, ip_address, port, url_scheme, is_active) + VALUES ($1, 1, $2, 'test-model', 'test-manufacturer', $3, $4, '50051', 'http', TRUE) + `, fixture.id, fixture.identifier, fixture.driverName, fmt.Sprintf("10.42.0.%d", i+1)) + require.NoError(t, err) + + if fixture.pairingStatus == "" { + continue + } + + _, err = conn.Exec(` + INSERT INTO device (id, org_id, discovered_device_id, device_identifier, mac_address) + VALUES ($1, 1, $1, $2, $3) + `, fixture.id, fixture.identifier, fmt.Sprintf("AA:BB:CC:DD:EE:%02d", i+1)) + require.NoError(t, err) + + _, err = conn.Exec(` + INSERT INTO device_pairing (device_id, pairing_status, paired_at) + VALUES ($1, $2, NOW()) + `, fixture.id, fixture.pairingStatus) + require.NoError(t, err) + + if fixture.fleetNodeOwned { + _, err = conn.Exec(` + INSERT INTO fleet_node_device (fleet_node_id, device_id, org_id) + VALUES ($1, $2, 1) + `, fleetNodeID, fixture.id) + require.NoError(t, err) + } + } + + rows, _, total, err := store.ListMinerStateSnapshots(ctx, 1, "", 100, &interfaces.MinerFilter{ + DeviceIdentifiers: identifiers, + }, nil) + require.NoError(t, err) + require.Equal(t, int64(len(fixtures)), total) + + byIdentifier := make(map[string]bool, len(rows)) + for _, row := range rows { + byIdentifier[row.DeviceIdentifier] = row.EmbeddedWebViewAvailable + } + + for _, fixture := range fixtures { + require.Equal(t, fixture.wantAvailable, byIdentifier[fixture.identifier], fixture.identifier) + } +} + // TestCountMinersByState_AuthNeededNullStatus verifies auth-needed miners with // NULL device_status go to broken_count, not offline_count. func TestCountMinersByState_AuthNeededNullStatus(t *testing.T) { diff --git a/server/internal/domain/stores/sqlstores/device_query_fragments.go b/server/internal/domain/stores/sqlstores/device_query_fragments.go index eb1c52ba8..0037f1ded 100644 --- a/server/internal/domain/stores/sqlstores/device_query_fragments.go +++ b/server/internal/domain/stores/sqlstores/device_query_fragments.go @@ -20,6 +20,16 @@ import ( // or the actual pairing status for paired devices. const pairingStatusExpr = "CASE WHEN device.id IS NOT NULL THEN COALESCE(device_pairing.pairing_status::text, 'UNPAIRED') ELSE 'UNPAIRED' END" +const embeddedWebViewAvailableExpr = `( + device.id IS NOT NULL + AND COALESCE(device_pairing.pairing_status::text, 'UNPAIRED') IN ('PAIRED', 'DEFAULT_PASSWORD') + AND discovered_device.driver_name = 'proto' + AND NOT EXISTS ( + SELECT 1 FROM fleet_node_device fnd + WHERE fnd.device_id = device.id AND fnd.org_id = device.org_id + ) +)` + // minerSelectColumns contains the common SELECT columns for miner state queries. const minerSelectColumns = `SELECT discovered_device.device_identifier, @@ -43,7 +53,8 @@ const minerSelectColumns = `SELECT device.site_id, COALESCE(site.name, '') as site_label, device.building_id, - COALESCE(building.name, '') as building_label` + COALESCE(building.name, '') as building_label, + ` + embeddedWebViewAvailableExpr + ` as embedded_web_view_available` // minerFromJoins contains the FROM clause and LEFT JOINs for miner state queries. // Parameter: $1 = org_id (used in device join condition) diff --git a/server/internal/handlers/minerproxy/handler.go b/server/internal/handlers/minerproxy/handler.go new file mode 100644 index 000000000..5ead903cd --- /dev/null +++ b/server/internal/handlers/minerproxy/handler.go @@ -0,0 +1,518 @@ +package minerproxy + +import ( + "bytes" + "context" + "crypto/tls" + "database/sql" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "connectrpc.com/authn" + + "github.com/block/proto-fleet/server/generated/sqlc" + "github.com/block/proto-fleet/server/internal/domain/authz" + "github.com/block/proto-fleet/server/internal/domain/fleeterror" + "github.com/block/proto-fleet/server/internal/domain/session" + stores "github.com/block/proto-fleet/server/internal/domain/stores/interfaces" + "github.com/block/proto-fleet/server/internal/handlers/middleware" + "github.com/block/proto-fleet/server/internal/infrastructure/db" + "github.com/block/proto-fleet/server/internal/infrastructure/encrypt" +) + +const ( + proxyClientTimeout = 30 * time.Second + maxProxyBodyBytes = 64 << 20 + defaultProtoURLScheme = "http" +) + +type errorResponse struct { + Error string `json:"error"` +} + +type loginRequest struct { + Password string `json:"password"` +} + +type loginResponse struct { + AccessToken string `json:"access_token"` +} + +type Handler struct { + queries *sqlc.Queries + sessionService *session.Service + userStore stores.UserStore + permissionResolver *authz.PermissionResolver + encryptService *encrypt.Service + httpClient *http.Client + httpsClient *http.Client + + tokenMu sync.Mutex + tokens map[string]string +} + +type proxyTarget struct { + deviceIdentifier string + baseURL string + siteID *int64 + passwordEnc sql.NullString +} + +func NewHandler( + conn *sql.DB, + sessionService *session.Service, + userStore stores.UserStore, + permissionResolver *authz.PermissionResolver, + encryptService *encrypt.Service, +) http.Handler { + return &Handler{ + queries: sqlc.New(db.NewRetryDB(conn)), + sessionService: sessionService, + userStore: userStore, + permissionResolver: permissionResolver, + encryptService: encryptService, + httpClient: newProxyHTTPClient(false), + httpsClient: newProxyHTTPClient(true), + tokens: make(map[string]string), + } +} + +func newProxyHTTPClient(skipVerify bool) *http.Client { + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: proxyClientTimeout, + ForceAttemptHTTP2: skipVerify, + } + if skipVerify { + transport.TLSClientConfig = &tls.Config{ + // Proto rigs commonly present self-signed certs. This matches the + // Proto plugin's transport behavior. + InsecureSkipVerify: true, // #nosec G402 -- intentional for Proto rig HTTPS + MinVersion: tls.VersionTLS12, + } + } + return &http.Client{ + Transport: transport, + Timeout: proxyClientTimeout, + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +func (h *Handler) clientFor(baseURL string) *http.Client { + if strings.HasPrefix(baseURL, "https://") { + return h.httpsClient + } + return h.httpClient +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, info, err := h.authenticate(r) + if err != nil { + slog.Warn("miner proxy authentication failed", "error", err) + writeError(w, httpStatusForError(err), "authentication required") + return + } + + deviceIdentifier := r.PathValue("deviceIdentifier") + rest := r.PathValue("rest") + if deviceIdentifier == "" || rest == "" { + writeError(w, http.StatusNotFound, "miner API route not found") + return + } + + target, err := h.resolveTarget(ctx, deviceIdentifier, info.OrganizationID) + if err != nil { + writeError(w, httpStatusForError(err), clientMessageForError(err)) + return + } + + proxyPath := "/api/v1/" + rest + requiredPermission := permissionFor(r.Method, proxyPath) + if _, err := middleware.RequirePermission(ctx, requiredPermission, authz.ResourceContext{SiteID: target.siteID}); err != nil { + writeError(w, httpStatusForError(err), clientMessageForError(err)) + return + } + + body, err := readProxyBody(w, r) + if err != nil { + writeError(w, http.StatusRequestEntityTooLarge, err.Error()) + return + } + + resp, err := h.forward(ctx, r, target, proxyPath, body, false) + if err != nil { + writeError(w, httpStatusForError(err), clientMessageForError(err)) + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized && target.passwordEnc.Valid { + _, _ = io.Copy(io.Discard, resp.Body) + h.clearToken(target.deviceIdentifier) + resp.Body.Close() + + resp, err = h.forward(ctx, r, target, proxyPath, body, true) + if err != nil { + writeError(w, httpStatusForError(err), clientMessageForError(err)) + return + } + defer resp.Body.Close() + } + + copyResponseHeaders(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + if r.Method == http.MethodHead { + return + } + if _, err := io.Copy(w, resp.Body); err != nil { + slog.Warn("miner proxy response copy failed", "device_identifier", deviceIdentifier, "error", err) + } +} + +func (h *Handler) authenticate(r *http.Request) (context.Context, *session.Info, error) { + cookie, err := r.Cookie(h.sessionService.CookieName()) + if err != nil || cookie.Value == "" { + return r.Context(), nil, fleeterror.NewUnauthenticatedError("session cookie required") + } + + sess, err := h.sessionService.Validate(r.Context(), cookie.Value) + if err != nil { + return r.Context(), nil, err + } + + user, err := h.userStore.GetUserByID(r.Context(), sess.UserID) + if err != nil { + return r.Context(), nil, fleeterror.NewUnauthenticatedErrorf("user with id %d not found", sess.UserID) + } + + info := &session.Info{ + AuthMethod: session.AuthMethodSession, + SessionID: sess.SessionID, + UserID: sess.UserID, + OrganizationID: sess.OrganizationID, + ExternalUserID: user.UserID, + Username: user.Username, + } + + effectivePermissions, err := h.permissionResolver.LoadEffective(r.Context(), info.UserID, info.OrganizationID) + if err != nil { + return r.Context(), nil, fleeterror.NewInternalErrorf("authz: effective permissions lookup failed: %v", err) + } + + ctx := middleware.WithEffectivePermissions(authn.SetInfo(r.Context(), info), effectivePermissions) + return ctx, info, nil +} + +func (h *Handler) resolveTarget(ctx context.Context, deviceIdentifier string, orgID int64) (proxyTarget, error) { + row, err := h.queries.GetDirectProtoMinerProxyTarget(ctx, sqlc.GetDirectProtoMinerProxyTargetParams{ + DeviceIdentifier: deviceIdentifier, + OrgID: orgID, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return proxyTarget{}, fleeterror.NewNotFoundError("miner not found or cannot be proxied") + } + return proxyTarget{}, fleeterror.NewInternalErrorf("failed to resolve miner proxy target: %v", err) + } + + scheme := strings.ToLower(row.UrlScheme) + if scheme == "" { + scheme = defaultProtoURLScheme + } + if scheme != "http" && scheme != "https" { + return proxyTarget{}, fleeterror.NewFailedPreconditionErrorf("miner URL scheme %q cannot be proxied", row.UrlScheme) + } + + host := row.IpAddress + if row.Port != "" && row.Port != "0" { + host = net.JoinHostPort(row.IpAddress, row.Port) + } + + base := url.URL{Scheme: scheme, Host: host} + var siteID *int64 + if row.SiteID.Valid { + site := row.SiteID.Int64 + siteID = &site + } + + return proxyTarget{ + deviceIdentifier: row.DeviceIdentifier, + baseURL: base.String(), + siteID: siteID, + passwordEnc: row.PasswordEnc, + }, nil +} + +func (h *Handler) forward( + ctx context.Context, + source *http.Request, + target proxyTarget, + proxyPath string, + body []byte, + forceLogin bool, +) (*http.Response, error) { + token, err := h.tokenFor(ctx, target, forceLogin) + if err != nil { + return nil, err + } + + targetURL := target.baseURL + proxyPath + if source.URL.RawQuery != "" { + targetURL += "?" + source.URL.RawQuery + } + + var bodyReader io.Reader + if body != nil { + bodyReader = bytes.NewReader(body) + } + req, err := http.NewRequestWithContext(ctx, source.Method, targetURL, bodyReader) + if err != nil { + return nil, fleeterror.NewInvalidArgumentErrorf("failed to create miner proxy request: %v", err) + } + copyRequestHeaders(req.Header, source.Header) + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } else { + req.Header.Del("Authorization") + } + + resp, err := h.clientFor(target.baseURL).Do(req) + if err != nil { + return nil, fleeterror.NewUnavailableErrorf("failed to reach miner: %v", err) + } + return resp, nil +} + +func (h *Handler) tokenFor(ctx context.Context, target proxyTarget, forceLogin bool) (string, error) { + if !target.passwordEnc.Valid || target.passwordEnc.String == "" { + return "", nil + } + + if !forceLogin { + h.tokenMu.Lock() + token := h.tokens[target.deviceIdentifier] + h.tokenMu.Unlock() + if token != "" { + return token, nil + } + } + + passwordBytes, err := h.encryptService.Decrypt(target.passwordEnc.String) + if err != nil { + return "", fleeterror.NewInternalErrorf("failed to decrypt miner credentials: %v", err) + } + + token, err := h.login(ctx, target.baseURL, string(passwordBytes)) + if err != nil { + return "", err + } + + h.tokenMu.Lock() + h.tokens[target.deviceIdentifier] = token + h.tokenMu.Unlock() + return token, nil +} + +func (h *Handler) clearToken(deviceIdentifier string) { + h.tokenMu.Lock() + defer h.tokenMu.Unlock() + delete(h.tokens, deviceIdentifier) +} + +func (h *Handler) login(ctx context.Context, baseURL string, password string) (string, error) { + body, err := json.Marshal(loginRequest{Password: password}) + if err != nil { + return "", fleeterror.NewInternalErrorf("failed to create miner login request: %v", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/v1/auth/login", bytes.NewReader(body)) + if err != nil { + return "", fleeterror.NewInternalErrorf("failed to create miner login request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := h.clientFor(baseURL).Do(req) + if err != nil { + return "", fleeterror.NewUnavailableErrorf("failed to authenticate with miner: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized { + _, _ = io.Copy(io.Discard, resp.Body) + return "", fleeterror.NewUnauthenticatedError("stored miner credentials were rejected") + } + if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, resp.Body) + return "", fleeterror.NewFailedPreconditionErrorf("miner login failed with status %d", resp.StatusCode) + } + + var tokens loginResponse + if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil { + return "", fleeterror.NewInternalErrorf("failed to decode miner login response: %v", err) + } + if tokens.AccessToken == "" { + return "", fleeterror.NewInternalError("miner login response did not include an access token") + } + return tokens.AccessToken, nil +} + +func readProxyBody(w http.ResponseWriter, r *http.Request) ([]byte, error) { + if r.Body == nil || r.Body == http.NoBody { + return nil, nil + } + defer r.Body.Close() + + reader := http.MaxBytesReader(w, r.Body, maxProxyBodyBytes) + body, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("proxied request body exceeds %d bytes", maxProxyBodyBytes) + } + if len(body) == 0 { + return nil, nil + } + return body, nil +} + +func permissionFor(method string, proxyPath string) string { + if strings.HasPrefix(proxyPath, "/api/v1/system/logs") { + return authz.PermMinerDownloadLogs + } + + if method == http.MethodGet || method == http.MethodHead { + return authz.PermMinerRead + } + + switch { + case proxyPath == "/api/v1/timeseries": + return authz.PermMinerRead + case strings.HasPrefix(proxyPath, "/api/v1/pools"): + return authz.PermMinerUpdatePools + case proxyPath == "/api/v1/cooling": + return authz.PermMinerSetCoolingMode + case proxyPath == "/api/v1/mining/target" || proxyPath == "/api/v1/mining/tuning": + return authz.PermMinerSetPowerTarget + case proxyPath == "/api/v1/mining/start": + return authz.PermMinerStartMining + case proxyPath == "/api/v1/mining/stop": + return authz.PermMinerStopMining + case proxyPath == "/api/v1/system/reboot": + return authz.PermMinerReboot + case proxyPath == "/api/v1/system/locate": + return authz.PermMinerBlinkLED + case strings.HasPrefix(proxyPath, "/api/v1/system/update"): + return authz.PermMinerFirmwareUpdate + case proxyPath == "/api/v1/power-supplies/update": + return authz.PermMinerFirmwareUpdate + case strings.HasPrefix(proxyPath, "/api/v1/system/tag"): + return authz.PermMinerRename + case strings.HasPrefix(proxyPath, "/api/v1/auth/"): + return authz.PermMinerUpdatePassword + case proxyPath == "/api/v1/system/ssh" || + proxyPath == "/api/v1/system/unlock" || + proxyPath == "/api/v1/network" || + proxyPath == "/api/v1/system/telemetry" || + strings.HasPrefix(proxyPath, "/api/v1/pairing/auth-key"): + return authz.PermMinerUpdatePassword + default: + // Device settings still need finer-grained Fleet permissions. Until + // then, require an elevated miner setting permission for any mutating + // ProtoOS endpoint that is not explicitly classified above. + return authz.PermMinerUpdatePassword + } +} + +func copyRequestHeaders(dst http.Header, src http.Header) { + for key, values := range src { + if shouldSkipRequestHeader(key) { + continue + } + for _, value := range values { + dst.Add(key, value) + } + } +} + +func copyResponseHeaders(dst http.Header, src http.Header) { + for key, values := range src { + if shouldSkipResponseHeader(key) { + continue + } + for _, value := range values { + dst.Add(key, value) + } + } +} + +func shouldSkipRequestHeader(key string) bool { + switch strings.ToLower(key) { + case "authorization", "cookie", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", + "te", "trailer", "transfer-encoding", "upgrade", "host": + return true + default: + return false + } +} + +func shouldSkipResponseHeader(key string) bool { + switch strings.ToLower(key) { + case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", + "transfer-encoding", "upgrade", "set-cookie": + return true + default: + return false + } +} + +func httpStatusForError(err error) int { + switch { + case fleeterror.IsAuthenticationError(err): + return http.StatusUnauthorized + case fleeterror.IsForbiddenError(err): + return http.StatusForbidden + case fleeterror.IsNotFoundError(err): + return http.StatusNotFound + case fleeterror.IsInvalidArgumentError(err): + return http.StatusBadRequest + case fleeterror.IsFailedPreconditionError(err): + return http.StatusPreconditionFailed + case fleeterror.IsUnavailableError(err): + return http.StatusBadGateway + default: + return http.StatusInternalServerError + } +} + +func clientMessageForError(err error) string { + switch { + case fleeterror.IsAuthenticationError(err): + return "authentication required" + case fleeterror.IsForbiddenError(err): + return "permission denied" + case fleeterror.IsNotFoundError(err): + return "miner not found or cannot be proxied" + default: + return err.Error() + } +} + +func writeError(w http.ResponseWriter, statusCode int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := json.NewEncoder(w).Encode(errorResponse{Error: message}); err != nil { + slog.Error("failed to encode miner proxy error response", "error", err) + } +} diff --git a/server/internal/handlers/minerproxy/handler_test.go b/server/internal/handlers/minerproxy/handler_test.go new file mode 100644 index 000000000..0702b2d63 --- /dev/null +++ b/server/internal/handlers/minerproxy/handler_test.go @@ -0,0 +1,108 @@ +package minerproxy + +import ( + "net/http" + "testing" + + "github.com/block/proto-fleet/server/internal/domain/authz" +) + +func TestPermissionFor(t *testing.T) { + tests := []struct { + name string + method string + proxyPath string + want string + }{ + { + name: "read endpoints require miner read", + method: http.MethodGet, + proxyPath: "/api/v1/network", + want: authz.PermMinerRead, + }, + { + name: "log downloads require log download permission for get", + method: http.MethodGet, + proxyPath: "/api/v1/system/logs", + want: authz.PermMinerDownloadLogs, + }, + { + name: "log downloads require log download permission for head", + method: http.MethodHead, + proxyPath: "/api/v1/system/logs", + want: authz.PermMinerDownloadLogs, + }, + { + name: "timeseries post is a read-style query", + method: http.MethodPost, + proxyPath: "/api/v1/timeseries", + want: authz.PermMinerRead, + }, + { + name: "pools writes require pool update", + method: http.MethodPut, + proxyPath: "/api/v1/pools/1", + want: authz.PermMinerUpdatePools, + }, + { + name: "power target writes require power target permission", + method: http.MethodPut, + proxyPath: "/api/v1/mining/target", + want: authz.PermMinerSetPowerTarget, + }, + { + name: "firmware writes require firmware update", + method: http.MethodPost, + proxyPath: "/api/v1/system/update", + want: authz.PermMinerFirmwareUpdate, + }, + { + name: "psu firmware writes require firmware update", + method: http.MethodPost, + proxyPath: "/api/v1/power-supplies/update", + want: authz.PermMinerFirmwareUpdate, + }, + { + name: "tag writes require rename", + method: http.MethodPut, + proxyPath: "/api/v1/system/tag", + want: authz.PermMinerRename, + }, + { + name: "security settings writes require password update", + method: http.MethodPut, + proxyPath: "/api/v1/system/ssh", + want: authz.PermMinerUpdatePassword, + }, + { + name: "unknown mutating endpoints do not fall through to read", + method: http.MethodPost, + proxyPath: "/api/v1/new-setting", + want: authz.PermMinerUpdatePassword, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := permissionFor(tt.method, tt.proxyPath); got != tt.want { + t.Fatalf("permissionFor(%q, %q) = %q, want %q", tt.method, tt.proxyPath, got, tt.want) + } + }) + } +} + +func TestCopyResponseHeadersDropsSetCookie(t *testing.T) { + src := http.Header{} + src.Add("Content-Type", "application/json") + src.Add("Set-Cookie", "miner_session=unsafe; Path=/") + + dst := http.Header{} + copyResponseHeaders(dst, src) + + if got := dst.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + if got := dst.Values("Set-Cookie"); len(got) != 0 { + t.Fatalf("Set-Cookie values = %v, want none", got) + } +} diff --git a/server/sqlc/queries/device.sql b/server/sqlc/queries/device.sql index 6cb1603cc..0093da31b 100644 --- a/server/sqlc/queries/device.sql +++ b/server/sqlc/queries/device.sql @@ -586,7 +586,8 @@ SELECT d.site_id, COALESCE(s.name, '') as site_label, d.building_id, - COALESCE(b.name, '') as building_label + COALESCE(b.name, '') as building_label, + FALSE as embedded_web_view_available FROM discovered_device dd LEFT JOIN device d ON dd.id = d.discovered_device_id LEFT JOIN device_pairing dp ON d.id = dp.device_id diff --git a/server/sqlc/queries/miner_service.sql b/server/sqlc/queries/miner_service.sql index a682229b1..6a683e3dc 100644 --- a/server/sqlc/queries/miner_service.sql +++ b/server/sqlc/queries/miner_service.sql @@ -29,6 +29,37 @@ WHERE d.device_identifier = $1 ) LIMIT 1; +-- name: GetDirectProtoMinerProxyTarget :one +SELECT + d.id, + d.device_identifier, + d.org_id, + d.site_id, + d.serial_number, + d.mac_address, + dd.ip_address, + dd.port, + dd.url_scheme, + dd.driver_name, + mc.username_enc, + mc.password_enc +FROM device d +JOIN discovered_device dd ON d.discovered_device_id = dd.id +JOIN device_pairing dp ON d.id = dp.device_id +LEFT JOIN miner_credentials mc ON d.id = mc.device_id +WHERE d.device_identifier = $1 + AND d.org_id = $2 + AND d.deleted_at IS NULL + AND dp.pairing_status IN ('PAIRED', 'DEFAULT_PASSWORD') + AND dd.driver_name = 'proto' + -- This HTTP proxy dials the miner from fleet-api. Fleet-node-owned + -- devices need a result-capable typed control command instead. + AND NOT EXISTS ( + SELECT 1 FROM fleet_node_device fnd + WHERE fnd.device_id = d.id AND fnd.org_id = d.org_id + ) +LIMIT 1; + -- name: GetActiveFleetNodeForDevice :one -- Resolve the active fleet node a device is paired to, with the connection -- coordinates the node needs to reach the LAN miner. The miner service calls this