feat(stats): admin instance resource statistics

pull/5925/head
Steven 4 weeks ago
parent cd4f28ae10
commit ea0625da45

@ -9,6 +9,7 @@ import "google/api/field_behavior.proto";
import "google/api/resource.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "google/type/color.proto";
option go_package = "gen/api/v1";
@ -41,6 +42,11 @@ service InstanceService {
body: "*"
};
}
// GetInstanceStats returns resource usage statistics for the instance. Admin only.
rpc GetInstanceStats(GetInstanceStatsRequest) returns (InstanceStats) {
option (google.api.http) = {get: "/api/v1/instance/stats"};
}
}
// Instance profile message containing basic instance information.
@ -272,3 +278,23 @@ message TestInstanceEmailSettingRequest {
// Optional. Recipient email address. If omitted, the current user's email address is used.
string recipient_email = 2 [(google.api.field_behavior) = OPTIONAL];
}
// Request message for GetInstanceStats.
message GetInstanceStatsRequest {}
// Resource usage statistics for the instance.
message InstanceStats {
DatabaseStats database = 1;
// Recursive size of the data directory in bytes. -1 if unavailable.
int64 local_storage_bytes = 2;
// Server-side timestamp when the snapshot was generated.
google.protobuf.Timestamp generated_time = 4;
// Database size statistics.
message DatabaseStats {
// driver is one of "sqlite", "mysql", "postgres".
string driver = 1;
// size_bytes is the database size in bytes; -1 if unavailable.
int64 size_bytes = 2;
}
}

@ -46,6 +46,9 @@ const (
// InstanceServiceTestInstanceEmailSettingProcedure is the fully-qualified name of the
// InstanceService's TestInstanceEmailSetting RPC.
InstanceServiceTestInstanceEmailSettingProcedure = "/memos.api.v1.InstanceService/TestInstanceEmailSetting"
// InstanceServiceGetInstanceStatsProcedure is the fully-qualified name of the InstanceService's
// GetInstanceStats RPC.
InstanceServiceGetInstanceStatsProcedure = "/memos.api.v1.InstanceService/GetInstanceStats"
)
// InstanceServiceClient is a client for the memos.api.v1.InstanceService service.
@ -58,6 +61,8 @@ type InstanceServiceClient interface {
UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)
// Tests notification email delivery with the provided or stored SMTP settings.
TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error)
// GetInstanceStats returns resource usage statistics for the instance. Admin only.
GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error)
}
// NewInstanceServiceClient constructs a client for the memos.api.v1.InstanceService service. By
@ -95,6 +100,12 @@ func NewInstanceServiceClient(httpClient connect.HTTPClient, baseURL string, opt
connect.WithSchema(instanceServiceMethods.ByName("TestInstanceEmailSetting")),
connect.WithClientOptions(opts...),
),
getInstanceStats: connect.NewClient[v1.GetInstanceStatsRequest, v1.InstanceStats](
httpClient,
baseURL+InstanceServiceGetInstanceStatsProcedure,
connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")),
connect.WithClientOptions(opts...),
),
}
}
@ -104,6 +115,7 @@ type instanceServiceClient struct {
getInstanceSetting *connect.Client[v1.GetInstanceSettingRequest, v1.InstanceSetting]
updateInstanceSetting *connect.Client[v1.UpdateInstanceSettingRequest, v1.InstanceSetting]
testInstanceEmailSetting *connect.Client[v1.TestInstanceEmailSettingRequest, emptypb.Empty]
getInstanceStats *connect.Client[v1.GetInstanceStatsRequest, v1.InstanceStats]
}
// GetInstanceProfile calls memos.api.v1.InstanceService.GetInstanceProfile.
@ -126,6 +138,11 @@ func (c *instanceServiceClient) TestInstanceEmailSetting(ctx context.Context, re
return c.testInstanceEmailSetting.CallUnary(ctx, req)
}
// GetInstanceStats calls memos.api.v1.InstanceService.GetInstanceStats.
func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, req *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) {
return c.getInstanceStats.CallUnary(ctx, req)
}
// InstanceServiceHandler is an implementation of the memos.api.v1.InstanceService service.
type InstanceServiceHandler interface {
// Gets the instance profile.
@ -136,6 +153,8 @@ type InstanceServiceHandler interface {
UpdateInstanceSetting(context.Context, *connect.Request[v1.UpdateInstanceSettingRequest]) (*connect.Response[v1.InstanceSetting], error)
// Tests notification email delivery with the provided or stored SMTP settings.
TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error)
// GetInstanceStats returns resource usage statistics for the instance. Admin only.
GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error)
}
// NewInstanceServiceHandler builds an HTTP handler from the service implementation. It returns the
@ -169,6 +188,12 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl
connect.WithSchema(instanceServiceMethods.ByName("TestInstanceEmailSetting")),
connect.WithHandlerOptions(opts...),
)
instanceServiceGetInstanceStatsHandler := connect.NewUnaryHandler(
InstanceServiceGetInstanceStatsProcedure,
svc.GetInstanceStats,
connect.WithSchema(instanceServiceMethods.ByName("GetInstanceStats")),
connect.WithHandlerOptions(opts...),
)
return "/memos.api.v1.InstanceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case InstanceServiceGetInstanceProfileProcedure:
@ -179,6 +204,8 @@ func NewInstanceServiceHandler(svc InstanceServiceHandler, opts ...connect.Handl
instanceServiceUpdateInstanceSettingHandler.ServeHTTP(w, r)
case InstanceServiceTestInstanceEmailSettingProcedure:
instanceServiceTestInstanceEmailSettingHandler.ServeHTTP(w, r)
case InstanceServiceGetInstanceStatsProcedure:
instanceServiceGetInstanceStatsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -203,3 +230,7 @@ func (UnimplementedInstanceServiceHandler) UpdateInstanceSetting(context.Context
func (UnimplementedInstanceServiceHandler) TestInstanceEmailSetting(context.Context, *connect.Request[v1.TestInstanceEmailSettingRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.TestInstanceEmailSetting is not implemented"))
}
func (UnimplementedInstanceServiceHandler) GetInstanceStats(context.Context, *connect.Request[v1.GetInstanceStatsRequest]) (*connect.Response[v1.InstanceStats], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.InstanceService.GetInstanceStats is not implemented"))
}

@ -13,6 +13,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
@ -633,6 +634,106 @@ func (x *TestInstanceEmailSettingRequest) GetRecipientEmail() string {
return ""
}
// Request message for GetInstanceStats.
type GetInstanceStatsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetInstanceStatsRequest) Reset() {
*x = GetInstanceStatsRequest{}
mi := &file_api_v1_instance_service_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetInstanceStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetInstanceStatsRequest) ProtoMessage() {}
func (x *GetInstanceStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetInstanceStatsRequest.ProtoReflect.Descriptor instead.
func (*GetInstanceStatsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{6}
}
// Resource usage statistics for the instance.
type InstanceStats struct {
state protoimpl.MessageState `protogen:"open.v1"`
Database *InstanceStats_DatabaseStats `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"`
// Recursive size of the data directory in bytes. -1 if unavailable.
LocalStorageBytes int64 `protobuf:"varint,2,opt,name=local_storage_bytes,json=localStorageBytes,proto3" json:"local_storage_bytes,omitempty"`
// Server-side timestamp when the snapshot was generated.
GeneratedTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=generated_time,json=generatedTime,proto3" json:"generated_time,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InstanceStats) Reset() {
*x = InstanceStats{}
mi := &file_api_v1_instance_service_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InstanceStats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InstanceStats) ProtoMessage() {}
func (x *InstanceStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InstanceStats.ProtoReflect.Descriptor instead.
func (*InstanceStats) Descriptor() ([]byte, []int) {
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{7}
}
func (x *InstanceStats) GetDatabase() *InstanceStats_DatabaseStats {
if x != nil {
return x.Database
}
return nil
}
func (x *InstanceStats) GetLocalStorageBytes() int64 {
if x != nil {
return x.LocalStorageBytes
}
return 0
}
func (x *InstanceStats) GetGeneratedTime() *timestamppb.Timestamp {
if x != nil {
return x.GeneratedTime
}
return nil
}
// General instance settings configuration.
type InstanceSetting_GeneralSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -660,7 +761,7 @@ type InstanceSetting_GeneralSetting struct {
func (x *InstanceSetting_GeneralSetting) Reset() {
*x = InstanceSetting_GeneralSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[6]
mi := &file_api_v1_instance_service_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -672,7 +773,7 @@ func (x *InstanceSetting_GeneralSetting) String() string {
func (*InstanceSetting_GeneralSetting) ProtoMessage() {}
func (x *InstanceSetting_GeneralSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[6]
mi := &file_api_v1_instance_service_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -762,7 +863,7 @@ type InstanceSetting_StorageSetting struct {
func (x *InstanceSetting_StorageSetting) Reset() {
*x = InstanceSetting_StorageSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[7]
mi := &file_api_v1_instance_service_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -774,7 +875,7 @@ func (x *InstanceSetting_StorageSetting) String() string {
func (*InstanceSetting_StorageSetting) ProtoMessage() {}
func (x *InstanceSetting_StorageSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[7]
mi := &file_api_v1_instance_service_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -833,7 +934,7 @@ type InstanceSetting_MemoRelatedSetting struct {
func (x *InstanceSetting_MemoRelatedSetting) Reset() {
*x = InstanceSetting_MemoRelatedSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[8]
mi := &file_api_v1_instance_service_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -845,7 +946,7 @@ func (x *InstanceSetting_MemoRelatedSetting) String() string {
func (*InstanceSetting_MemoRelatedSetting) ProtoMessage() {}
func (x *InstanceSetting_MemoRelatedSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[8]
mi := &file_api_v1_instance_service_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -896,7 +997,7 @@ type InstanceSetting_TagMetadata struct {
func (x *InstanceSetting_TagMetadata) Reset() {
*x = InstanceSetting_TagMetadata{}
mi := &file_api_v1_instance_service_proto_msgTypes[9]
mi := &file_api_v1_instance_service_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -908,7 +1009,7 @@ func (x *InstanceSetting_TagMetadata) String() string {
func (*InstanceSetting_TagMetadata) ProtoMessage() {}
func (x *InstanceSetting_TagMetadata) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[9]
mi := &file_api_v1_instance_service_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -952,7 +1053,7 @@ type InstanceSetting_TagsSetting struct {
func (x *InstanceSetting_TagsSetting) Reset() {
*x = InstanceSetting_TagsSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[10]
mi := &file_api_v1_instance_service_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -964,7 +1065,7 @@ func (x *InstanceSetting_TagsSetting) String() string {
func (*InstanceSetting_TagsSetting) ProtoMessage() {}
func (x *InstanceSetting_TagsSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[10]
mi := &file_api_v1_instance_service_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -997,7 +1098,7 @@ type InstanceSetting_NotificationSetting struct {
func (x *InstanceSetting_NotificationSetting) Reset() {
*x = InstanceSetting_NotificationSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[11]
mi := &file_api_v1_instance_service_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1009,7 +1110,7 @@ func (x *InstanceSetting_NotificationSetting) String() string {
func (*InstanceSetting_NotificationSetting) ProtoMessage() {}
func (x *InstanceSetting_NotificationSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[11]
mi := &file_api_v1_instance_service_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1043,7 +1144,7 @@ type InstanceSetting_AISetting struct {
func (x *InstanceSetting_AISetting) Reset() {
*x = InstanceSetting_AISetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[12]
mi := &file_api_v1_instance_service_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1055,7 +1156,7 @@ func (x *InstanceSetting_AISetting) String() string {
func (*InstanceSetting_AISetting) ProtoMessage() {}
func (x *InstanceSetting_AISetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[12]
mi := &file_api_v1_instance_service_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1097,7 +1198,7 @@ type InstanceSetting_AIProviderConfig struct {
func (x *InstanceSetting_AIProviderConfig) Reset() {
*x = InstanceSetting_AIProviderConfig{}
mi := &file_api_v1_instance_service_proto_msgTypes[13]
mi := &file_api_v1_instance_service_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1109,7 +1210,7 @@ func (x *InstanceSetting_AIProviderConfig) String() string {
func (*InstanceSetting_AIProviderConfig) ProtoMessage() {}
func (x *InstanceSetting_AIProviderConfig) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[13]
mi := &file_api_v1_instance_service_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1186,7 +1287,7 @@ type InstanceSetting_GeneralSetting_CustomProfile struct {
func (x *InstanceSetting_GeneralSetting_CustomProfile) Reset() {
*x = InstanceSetting_GeneralSetting_CustomProfile{}
mi := &file_api_v1_instance_service_proto_msgTypes[14]
mi := &file_api_v1_instance_service_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1198,7 +1299,7 @@ func (x *InstanceSetting_GeneralSetting_CustomProfile) String() string {
func (*InstanceSetting_GeneralSetting_CustomProfile) ProtoMessage() {}
func (x *InstanceSetting_GeneralSetting_CustomProfile) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[14]
mi := &file_api_v1_instance_service_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1251,7 +1352,7 @@ type InstanceSetting_StorageSetting_S3Config struct {
func (x *InstanceSetting_StorageSetting_S3Config) Reset() {
*x = InstanceSetting_StorageSetting_S3Config{}
mi := &file_api_v1_instance_service_proto_msgTypes[15]
mi := &file_api_v1_instance_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1263,7 +1364,7 @@ func (x *InstanceSetting_StorageSetting_S3Config) String() string {
func (*InstanceSetting_StorageSetting_S3Config) ProtoMessage() {}
func (x *InstanceSetting_StorageSetting_S3Config) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[15]
mi := &file_api_v1_instance_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1340,7 +1441,7 @@ type InstanceSetting_NotificationSetting_EmailSetting struct {
func (x *InstanceSetting_NotificationSetting_EmailSetting) Reset() {
*x = InstanceSetting_NotificationSetting_EmailSetting{}
mi := &file_api_v1_instance_service_proto_msgTypes[17]
mi := &file_api_v1_instance_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1352,7 +1453,7 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) String() string {
func (*InstanceSetting_NotificationSetting_EmailSetting) ProtoMessage() {}
func (x *InstanceSetting_NotificationSetting_EmailSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[17]
mi := &file_api_v1_instance_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1438,11 +1539,66 @@ func (x *InstanceSetting_NotificationSetting_EmailSetting) GetUseSsl() bool {
return false
}
// Database size statistics.
type InstanceStats_DatabaseStats struct {
state protoimpl.MessageState `protogen:"open.v1"`
// driver is one of "sqlite", "mysql", "postgres".
Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
// size_bytes is the database size in bytes; -1 if unavailable.
SizeBytes int64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InstanceStats_DatabaseStats) Reset() {
*x = InstanceStats_DatabaseStats{}
mi := &file_api_v1_instance_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InstanceStats_DatabaseStats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InstanceStats_DatabaseStats) ProtoMessage() {}
func (x *InstanceStats_DatabaseStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_instance_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InstanceStats_DatabaseStats.ProtoReflect.Descriptor instead.
func (*InstanceStats_DatabaseStats) Descriptor() ([]byte, []int) {
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{7, 0}
}
func (x *InstanceStats_DatabaseStats) GetDriver() string {
if x != nil {
return x.Driver
}
return ""
}
func (x *InstanceStats_DatabaseStats) GetSizeBytes() int64 {
if x != nil {
return x.SizeBytes
}
return 0
}
var File_api_v1_instance_service_proto protoreflect.FileDescriptor
const file_api_v1_instance_service_proto_rawDesc = "" +
"\n" +
"\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x17google/type/color.proto\"\xa4\x01\n" +
"\x1dapi/v1/instance_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17google/type/color.proto\"\xa4\x01\n" +
"\x0fInstanceProfile\x12\x18\n" +
"\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" +
"\x04demo\x18\x03 \x01(\bR\x04demo\x12!\n" +
@ -1552,12 +1708,22 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"updateMask\"\xaa\x01\n" +
"\x1fTestInstanceEmailSettingRequest\x12Y\n" +
"\x05email\x18\x01 \x01(\v2>.memos.api.v1.InstanceSetting.NotificationSetting.EmailSettingB\x03\xe0A\x01R\x05email\x12,\n" +
"\x0frecipient_email\x18\x02 \x01(\tB\x03\xe0A\x01R\x0erecipientEmail2\xfc\x04\n" +
"\x0frecipient_email\x18\x02 \x01(\tB\x03\xe0A\x01R\x0erecipientEmail\"\x19\n" +
"\x17GetInstanceStatsRequest\"\x91\x02\n" +
"\rInstanceStats\x12E\n" +
"\bdatabase\x18\x01 \x01(\v2).memos.api.v1.InstanceStats.DatabaseStatsR\bdatabase\x12.\n" +
"\x13local_storage_bytes\x18\x02 \x01(\x03R\x11localStorageBytes\x12A\n" +
"\x0egenerated_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\rgeneratedTime\x1aF\n" +
"\rDatabaseStats\x12\x16\n" +
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x1d\n" +
"\n" +
"size_bytes\x18\x02 \x01(\x03R\tsizeBytes2\xf4\x05\n" +
"\x0fInstanceService\x12~\n" +
"\x12GetInstanceProfile\x12'.memos.api.v1.GetInstanceProfileRequest\x1a\x1d.memos.api.v1.InstanceProfile\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/instance/profile\x12\x8f\x01\n" +
"\x12GetInstanceSetting\x12'.memos.api.v1.GetInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=instance/settings/*}\x12\xb5\x01\n" +
"\x15UpdateInstanceSetting\x12*.memos.api.v1.UpdateInstanceSettingRequest\x1a\x1d.memos.api.v1.InstanceSetting\"Q\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x025:\asetting2*/api/v1/{setting.name=instance/settings/*}\x12\x9e\x01\n" +
"\x18TestInstanceEmailSetting\x12-.memos.api.v1.TestInstanceEmailSettingRequest\x1a\x16.google.protobuf.Empty\";\x82\xd3\xe4\x93\x025:\x01*\"0/api/v1/instance/settings/notification:testEmailB\xac\x01\n" +
"\x18TestInstanceEmailSetting\x12-.memos.api.v1.TestInstanceEmailSettingRequest\x1a\x16.google.protobuf.Empty\";\x82\xd3\xe4\x93\x025:\x01*\"0/api/v1/instance/settings/notification:testEmail\x12v\n" +
"\x10GetInstanceStats\x12%.memos.api.v1.GetInstanceStatsRequest\x1a\x1b.memos.api.v1.InstanceStats\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/instance/statsB\xac\x01\n" +
"\x10com.memos.api.v1B\x14InstanceServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3"
var (
@ -1573,7 +1739,7 @@ func file_api_v1_instance_service_proto_rawDescGZIP() []byte {
}
var file_api_v1_instance_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
var file_api_v1_instance_service_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_api_v1_instance_service_proto_goTypes = []any{
(InstanceSetting_Key)(0), // 0: memos.api.v1.InstanceSetting.Key
(InstanceSetting_AIProviderType)(0), // 1: memos.api.v1.InstanceSetting.AIProviderType
@ -1584,56 +1750,64 @@ var file_api_v1_instance_service_proto_goTypes = []any{
(*GetInstanceSettingRequest)(nil), // 6: memos.api.v1.GetInstanceSettingRequest
(*UpdateInstanceSettingRequest)(nil), // 7: memos.api.v1.UpdateInstanceSettingRequest
(*TestInstanceEmailSettingRequest)(nil), // 8: memos.api.v1.TestInstanceEmailSettingRequest
(*InstanceSetting_GeneralSetting)(nil), // 9: memos.api.v1.InstanceSetting.GeneralSetting
(*InstanceSetting_StorageSetting)(nil), // 10: memos.api.v1.InstanceSetting.StorageSetting
(*InstanceSetting_MemoRelatedSetting)(nil), // 11: memos.api.v1.InstanceSetting.MemoRelatedSetting
(*InstanceSetting_TagMetadata)(nil), // 12: memos.api.v1.InstanceSetting.TagMetadata
(*InstanceSetting_TagsSetting)(nil), // 13: memos.api.v1.InstanceSetting.TagsSetting
(*InstanceSetting_NotificationSetting)(nil), // 14: memos.api.v1.InstanceSetting.NotificationSetting
(*InstanceSetting_AISetting)(nil), // 15: memos.api.v1.InstanceSetting.AISetting
(*InstanceSetting_AIProviderConfig)(nil), // 16: memos.api.v1.InstanceSetting.AIProviderConfig
(*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 17: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
(*InstanceSetting_StorageSetting_S3Config)(nil), // 18: memos.api.v1.InstanceSetting.StorageSetting.S3Config
nil, // 19: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry
(*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 20: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
(*User)(nil), // 21: memos.api.v1.User
(*fieldmaskpb.FieldMask)(nil), // 22: google.protobuf.FieldMask
(*color.Color)(nil), // 23: google.type.Color
(*emptypb.Empty)(nil), // 24: google.protobuf.Empty
(*GetInstanceStatsRequest)(nil), // 9: memos.api.v1.GetInstanceStatsRequest
(*InstanceStats)(nil), // 10: memos.api.v1.InstanceStats
(*InstanceSetting_GeneralSetting)(nil), // 11: memos.api.v1.InstanceSetting.GeneralSetting
(*InstanceSetting_StorageSetting)(nil), // 12: memos.api.v1.InstanceSetting.StorageSetting
(*InstanceSetting_MemoRelatedSetting)(nil), // 13: memos.api.v1.InstanceSetting.MemoRelatedSetting
(*InstanceSetting_TagMetadata)(nil), // 14: memos.api.v1.InstanceSetting.TagMetadata
(*InstanceSetting_TagsSetting)(nil), // 15: memos.api.v1.InstanceSetting.TagsSetting
(*InstanceSetting_NotificationSetting)(nil), // 16: memos.api.v1.InstanceSetting.NotificationSetting
(*InstanceSetting_AISetting)(nil), // 17: memos.api.v1.InstanceSetting.AISetting
(*InstanceSetting_AIProviderConfig)(nil), // 18: memos.api.v1.InstanceSetting.AIProviderConfig
(*InstanceSetting_GeneralSetting_CustomProfile)(nil), // 19: memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
(*InstanceSetting_StorageSetting_S3Config)(nil), // 20: memos.api.v1.InstanceSetting.StorageSetting.S3Config
nil, // 21: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry
(*InstanceSetting_NotificationSetting_EmailSetting)(nil), // 22: memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
(*InstanceStats_DatabaseStats)(nil), // 23: memos.api.v1.InstanceStats.DatabaseStats
(*User)(nil), // 24: memos.api.v1.User
(*fieldmaskpb.FieldMask)(nil), // 25: google.protobuf.FieldMask
(*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp
(*color.Color)(nil), // 27: google.type.Color
(*emptypb.Empty)(nil), // 28: google.protobuf.Empty
}
var file_api_v1_instance_service_proto_depIdxs = []int32{
21, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User
9, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting
10, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting
11, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting
13, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting
14, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting
15, // 6: memos.api.v1.InstanceSetting.ai_setting:type_name -> memos.api.v1.InstanceSetting.AISetting
24, // 0: memos.api.v1.InstanceProfile.admin:type_name -> memos.api.v1.User
11, // 1: memos.api.v1.InstanceSetting.general_setting:type_name -> memos.api.v1.InstanceSetting.GeneralSetting
12, // 2: memos.api.v1.InstanceSetting.storage_setting:type_name -> memos.api.v1.InstanceSetting.StorageSetting
13, // 3: memos.api.v1.InstanceSetting.memo_related_setting:type_name -> memos.api.v1.InstanceSetting.MemoRelatedSetting
15, // 4: memos.api.v1.InstanceSetting.tags_setting:type_name -> memos.api.v1.InstanceSetting.TagsSetting
16, // 5: memos.api.v1.InstanceSetting.notification_setting:type_name -> memos.api.v1.InstanceSetting.NotificationSetting
17, // 6: memos.api.v1.InstanceSetting.ai_setting:type_name -> memos.api.v1.InstanceSetting.AISetting
5, // 7: memos.api.v1.UpdateInstanceSettingRequest.setting:type_name -> memos.api.v1.InstanceSetting
22, // 8: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
20, // 9: memos.api.v1.TestInstanceEmailSettingRequest.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
17, // 10: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
2, // 11: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType
18, // 12: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config
23, // 13: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color
19, // 14: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry
20, // 15: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
16, // 16: memos.api.v1.InstanceSetting.AISetting.providers:type_name -> memos.api.v1.InstanceSetting.AIProviderConfig
1, // 17: memos.api.v1.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.v1.InstanceSetting.AIProviderType
12, // 18: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata
4, // 19: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest
6, // 20: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest
7, // 21: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest
8, // 22: memos.api.v1.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.v1.TestInstanceEmailSettingRequest
3, // 23: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile
5, // 24: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting
5, // 25: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting
24, // 26: memos.api.v1.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty
23, // [23:27] is the sub-list for method output_type
19, // [19:23] is the sub-list for method input_type
19, // [19:19] is the sub-list for extension type_name
19, // [19:19] is the sub-list for extension extendee
0, // [0:19] is the sub-list for field type_name
25, // 8: memos.api.v1.UpdateInstanceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
22, // 9: memos.api.v1.TestInstanceEmailSettingRequest.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
23, // 10: memos.api.v1.InstanceStats.database:type_name -> memos.api.v1.InstanceStats.DatabaseStats
26, // 11: memos.api.v1.InstanceStats.generated_time:type_name -> google.protobuf.Timestamp
19, // 12: memos.api.v1.InstanceSetting.GeneralSetting.custom_profile:type_name -> memos.api.v1.InstanceSetting.GeneralSetting.CustomProfile
2, // 13: memos.api.v1.InstanceSetting.StorageSetting.storage_type:type_name -> memos.api.v1.InstanceSetting.StorageSetting.StorageType
20, // 14: memos.api.v1.InstanceSetting.StorageSetting.s3_config:type_name -> memos.api.v1.InstanceSetting.StorageSetting.S3Config
27, // 15: memos.api.v1.InstanceSetting.TagMetadata.background_color:type_name -> google.type.Color
21, // 16: memos.api.v1.InstanceSetting.TagsSetting.tags:type_name -> memos.api.v1.InstanceSetting.TagsSetting.TagsEntry
22, // 17: memos.api.v1.InstanceSetting.NotificationSetting.email:type_name -> memos.api.v1.InstanceSetting.NotificationSetting.EmailSetting
18, // 18: memos.api.v1.InstanceSetting.AISetting.providers:type_name -> memos.api.v1.InstanceSetting.AIProviderConfig
1, // 19: memos.api.v1.InstanceSetting.AIProviderConfig.type:type_name -> memos.api.v1.InstanceSetting.AIProviderType
14, // 20: memos.api.v1.InstanceSetting.TagsSetting.TagsEntry.value:type_name -> memos.api.v1.InstanceSetting.TagMetadata
4, // 21: memos.api.v1.InstanceService.GetInstanceProfile:input_type -> memos.api.v1.GetInstanceProfileRequest
6, // 22: memos.api.v1.InstanceService.GetInstanceSetting:input_type -> memos.api.v1.GetInstanceSettingRequest
7, // 23: memos.api.v1.InstanceService.UpdateInstanceSetting:input_type -> memos.api.v1.UpdateInstanceSettingRequest
8, // 24: memos.api.v1.InstanceService.TestInstanceEmailSetting:input_type -> memos.api.v1.TestInstanceEmailSettingRequest
9, // 25: memos.api.v1.InstanceService.GetInstanceStats:input_type -> memos.api.v1.GetInstanceStatsRequest
3, // 26: memos.api.v1.InstanceService.GetInstanceProfile:output_type -> memos.api.v1.InstanceProfile
5, // 27: memos.api.v1.InstanceService.GetInstanceSetting:output_type -> memos.api.v1.InstanceSetting
5, // 28: memos.api.v1.InstanceService.UpdateInstanceSetting:output_type -> memos.api.v1.InstanceSetting
28, // 29: memos.api.v1.InstanceService.TestInstanceEmailSetting:output_type -> google.protobuf.Empty
10, // 30: memos.api.v1.InstanceService.GetInstanceStats:output_type -> memos.api.v1.InstanceStats
26, // [26:31] is the sub-list for method output_type
21, // [21:26] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
}
func init() { file_api_v1_instance_service_proto_init() }
@ -1656,7 +1830,7 @@ func file_api_v1_instance_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_instance_service_proto_rawDesc), len(file_api_v1_instance_service_proto_rawDesc)),
NumEnums: 3,
NumMessages: 18,
NumMessages: 21,
NumExtensions: 0,
NumServices: 1,
},

@ -203,6 +203,27 @@ func local_request_InstanceService_TestInstanceEmailSetting_0(ctx context.Contex
return msg, metadata, err
}
func request_InstanceService_GetInstanceStats_0(ctx context.Context, marshaler runtime.Marshaler, client InstanceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetInstanceStatsRequest
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetInstanceStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_InstanceService_GetInstanceStats_0(ctx context.Context, marshaler runtime.Marshaler, server InstanceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetInstanceStatsRequest
metadata runtime.ServerMetadata
)
msg, err := server.GetInstanceStats(ctx, &protoReq)
return msg, metadata, err
}
// RegisterInstanceServiceHandlerServer registers the http handlers for service InstanceService to "mux".
// UnaryRPC :call InstanceServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -289,6 +310,26 @@ func RegisterInstanceServiceHandlerServer(ctx context.Context, mux *runtime.Serv
}
forward_InstanceService_TestInstanceEmailSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InstanceService/GetInstanceStats", runtime.WithHTTPPathPattern("/api/v1/instance/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_InstanceService_GetInstanceStats_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -397,6 +438,23 @@ func RegisterInstanceServiceHandlerClient(ctx context.Context, mux *runtime.Serv
}
forward_InstanceService_TestInstanceEmailSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_InstanceService_GetInstanceStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InstanceService/GetInstanceStats", runtime.WithHTTPPathPattern("/api/v1/instance/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_InstanceService_GetInstanceStats_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_InstanceService_GetInstanceStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -405,6 +463,7 @@ var (
pattern_InstanceService_GetInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "name"}, ""))
pattern_InstanceService_UpdateInstanceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "instance", "settings", "setting.name"}, ""))
pattern_InstanceService_TestInstanceEmailSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "instance", "settings", "notification"}, "testEmail"))
pattern_InstanceService_GetInstanceStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "instance", "stats"}, ""))
)
var (
@ -412,4 +471,5 @@ var (
forward_InstanceService_GetInstanceSetting_0 = runtime.ForwardResponseMessage
forward_InstanceService_UpdateInstanceSetting_0 = runtime.ForwardResponseMessage
forward_InstanceService_TestInstanceEmailSetting_0 = runtime.ForwardResponseMessage
forward_InstanceService_GetInstanceStats_0 = runtime.ForwardResponseMessage
)

@ -24,6 +24,7 @@ const (
InstanceService_GetInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceSetting"
InstanceService_UpdateInstanceSetting_FullMethodName = "/memos.api.v1.InstanceService/UpdateInstanceSetting"
InstanceService_TestInstanceEmailSetting_FullMethodName = "/memos.api.v1.InstanceService/TestInstanceEmailSetting"
InstanceService_GetInstanceStats_FullMethodName = "/memos.api.v1.InstanceService/GetInstanceStats"
)
// InstanceServiceClient is the client API for InstanceService service.
@ -38,6 +39,8 @@ type InstanceServiceClient interface {
UpdateInstanceSetting(ctx context.Context, in *UpdateInstanceSettingRequest, opts ...grpc.CallOption) (*InstanceSetting, error)
// Tests notification email delivery with the provided or stored SMTP settings.
TestInstanceEmailSetting(ctx context.Context, in *TestInstanceEmailSettingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// GetInstanceStats returns resource usage statistics for the instance. Admin only.
GetInstanceStats(ctx context.Context, in *GetInstanceStatsRequest, opts ...grpc.CallOption) (*InstanceStats, error)
}
type instanceServiceClient struct {
@ -88,6 +91,16 @@ func (c *instanceServiceClient) TestInstanceEmailSetting(ctx context.Context, in
return out, nil
}
func (c *instanceServiceClient) GetInstanceStats(ctx context.Context, in *GetInstanceStatsRequest, opts ...grpc.CallOption) (*InstanceStats, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(InstanceStats)
err := c.cc.Invoke(ctx, InstanceService_GetInstanceStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// InstanceServiceServer is the server API for InstanceService service.
// All implementations must embed UnimplementedInstanceServiceServer
// for forward compatibility.
@ -100,6 +113,8 @@ type InstanceServiceServer interface {
UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error)
// Tests notification email delivery with the provided or stored SMTP settings.
TestInstanceEmailSetting(context.Context, *TestInstanceEmailSettingRequest) (*emptypb.Empty, error)
// GetInstanceStats returns resource usage statistics for the instance. Admin only.
GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error)
mustEmbedUnimplementedInstanceServiceServer()
}
@ -122,6 +137,9 @@ func (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context,
func (UnimplementedInstanceServiceServer) TestInstanceEmailSetting(context.Context, *TestInstanceEmailSettingRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TestInstanceEmailSetting not implemented")
}
func (UnimplementedInstanceServiceServer) GetInstanceStats(context.Context, *GetInstanceStatsRequest) (*InstanceStats, error) {
return nil, status.Error(codes.Unimplemented, "method GetInstanceStats not implemented")
}
func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {}
func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {}
@ -215,6 +233,24 @@ func _InstanceService_TestInstanceEmailSetting_Handler(srv interface{}, ctx cont
return interceptor(ctx, in, info, handler)
}
func _InstanceService_GetInstanceStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetInstanceStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(InstanceServiceServer).GetInstanceStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: InstanceService_GetInstanceStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(InstanceServiceServer).GetInstanceStats(ctx, req.(*GetInstanceStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
// InstanceService_ServiceDesc is the grpc.ServiceDesc for InstanceService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -238,6 +274,10 @@ var InstanceService_ServiceDesc = grpc.ServiceDesc{
MethodName: "TestInstanceEmailSetting",
Handler: _InstanceService_TestInstanceEmailSetting_Handler,
},
{
MethodName: "GetInstanceStats",
Handler: _InstanceService_GetInstanceStats_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/v1/instance_service.proto",

@ -498,6 +498,25 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/instance/stats:
get:
tags:
- InstanceService
description: GetInstanceStats returns resource usage statistics for the instance. Admin only.
operationId: InstanceService_GetInstanceStats
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/InstanceStats'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/instance/{instance}/*:
get:
tags:
@ -2789,6 +2808,29 @@ components:
so a single entry like "project/.*" matches all tags under that prefix.
Exact tag names are also valid (they are trivially valid regex patterns).
description: Tag metadata configuration.
InstanceStats:
type: object
properties:
database:
$ref: '#/components/schemas/InstanceStats_DatabaseStats'
localStorageBytes:
type: string
description: Recursive size of the data directory in bytes. -1 if unavailable.
generatedTime:
type: string
description: Server-side timestamp when the snapshot was generated.
format: date-time
description: Resource usage statistics for the instance.
InstanceStats_DatabaseStats:
type: object
properties:
driver:
type: string
description: driver is one of "sqlite", "mysql", "postgres".
sizeBytes:
type: string
description: size_bytes is the database size in bytes; -1 if unavailable.
description: Database size statistics.
LinkMetadata:
type: object
properties:

@ -47,6 +47,14 @@ func (s *ConnectServiceHandler) TestInstanceEmailSetting(ctx context.Context, re
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) GetInstanceStats(ctx context.Context, req *connect.Request[v1pb.GetInstanceStatsRequest]) (*connect.Response[v1pb.InstanceStats], error) {
resp, err := s.APIV1Service.GetInstanceStats(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
// AuthService
//
// Auth service methods need special handling for response headers (cookies).

@ -0,0 +1,165 @@
package v1
import (
"context"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
const instanceStatsCacheTTL = 60 * time.Second
// instanceStatsCache is a single-value, mutex-guarded cache for InstanceStats.
type instanceStatsCache struct {
mu sync.Mutex
value *v1pb.InstanceStats
expiry time.Time
}
func (c *instanceStatsCache) get() (*v1pb.InstanceStats, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.value == nil || time.Now().After(c.expiry) {
return nil, false
}
return c.value, true
}
func (c *instanceStatsCache) set(v *v1pb.InstanceStats, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.value = v
c.expiry = time.Now().Add(ttl)
}
// GetInstanceStats returns resource usage statistics. Admin only.
func (s *APIV1Service) GetInstanceStats(ctx context.Context, _ *v1pb.GetInstanceStatsRequest) (*v1pb.InstanceStats, error) {
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if user.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if cached, ok := s.instanceStatsCache.get(); ok {
return cached, nil
}
stats, err := s.computeInstanceStats(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to compute instance stats: %v", err)
}
s.instanceStatsCache.set(stats, instanceStatsCacheTTL)
return stats, nil
}
// computeInstanceStats runs all stat subqueries in parallel and assembles the result.
// Per-subtask failures degrade to -1 sentinel values; only a total failure (every
// subtask errored) is propagated as an error.
func (s *APIV1Service) computeInstanceStats(ctx context.Context) (*v1pb.InstanceStats, error) {
stats := &v1pb.InstanceStats{
Database: &v1pb.InstanceStats_DatabaseStats{
Driver: s.Profile.Driver,
SizeBytes: -1,
},
LocalStorageBytes: -1,
GeneratedTime: timestamppb.Now(),
}
type result struct {
name string
err error
}
var (
mu sync.Mutex
results []result
record = func(name string, err error) {
mu.Lock()
results = append(results, result{name, err})
mu.Unlock()
}
)
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
size, err := s.Store.GetDriver().GetDatabaseSize(gctx)
if err != nil {
record("database_size", err)
return nil
}
stats.Database.SizeBytes = size
return nil
})
g.Go(func() error {
size, err := walkLocalStorage(s.Profile.Data)
if err != nil {
record("local_storage", err)
return nil
}
stats.LocalStorageBytes = size
return nil
})
_ = g.Wait()
for _, r := range results {
slog.Warn("instance stats subtask failed", slog.String("subtask", r.name), slog.String("err", r.err.Error()))
}
const totalSubtasks = 2
if len(results) == totalSubtasks {
return nil, errors.New("all instance stats subtasks failed")
}
return stats, nil
}
// walkLocalStorage returns the recursive size of dir in bytes.
// Symlinks are not followed; per-entry errors below the root are ignored
// (the walk continues). An error accessing the root itself is returned.
func walkLocalStorage(dir string) (int64, error) {
if dir == "" {
return -1, errors.New("empty data directory")
}
var total int64
err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
if path == dir {
// Root itself is inaccessible — abort the walk.
return walkErr
}
// Ignore per-entry errors (e.g. permission denied on a single file).
return nil
}
if entry.IsDir() {
return nil
}
info, err := entry.Info()
if err != nil {
// Ignore stat errors on individual entries; continue the walk.
return nil //nolint:nilerr
}
total += info.Size()
return nil
})
if err != nil {
return -1, errors.Wrap(err, "walk failed")
}
return total, nil
}

@ -0,0 +1,34 @@
package v1
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestWalkLocalStorage_SumsFileSizes(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello"), 0o600)) // 5
require.NoError(t, os.WriteFile(filepath.Join(dir, "b.txt"), []byte("world!"), 0o600)) // 6
sub := filepath.Join(dir, "sub")
require.NoError(t, os.Mkdir(sub, 0o700))
require.NoError(t, os.WriteFile(filepath.Join(sub, "c.txt"), []byte("xx"), 0o600)) // 2
size, err := walkLocalStorage(dir)
require.NoError(t, err)
require.Equal(t, int64(13), size)
}
func TestWalkLocalStorage_EmptyDir(t *testing.T) {
size, err := walkLocalStorage("")
require.Error(t, err)
require.Equal(t, int64(-1), size)
}
func TestWalkLocalStorage_NonexistentDir(t *testing.T) {
size, err := walkLocalStorage(filepath.Join(t.TempDir(), "does-not-exist"))
require.Error(t, err)
require.Equal(t, int64(-1), size)
}

@ -0,0 +1,72 @@
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
)
func TestGetInstanceStats_HappyPath(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
admin, err := ts.CreateHostUser(ctx, "admin1")
require.NoError(t, err)
adminCtx := ts.CreateUserContext(ctx, admin.ID)
resp, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Database)
require.Equal(t, "sqlite", resp.Database.Driver)
require.Greater(t, resp.Database.SizeBytes, int64(0))
require.GreaterOrEqual(t, resp.LocalStorageBytes, int64(0))
}
func TestGetInstanceStats_NonAdminDenied(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
// Need an admin to exist (otherwise instance is uninitialized).
admin, err := ts.CreateHostUser(ctx, "admin1")
require.NoError(t, err)
_ = admin
regular, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
regularCtx := ts.CreateUserContext(ctx, regular.ID)
_, err = ts.Service.GetInstanceStats(regularCtx, &v1pb.GetInstanceStatsRequest{})
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.PermissionDenied, st.Code())
}
func TestGetInstanceStats_Cache(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
admin, err := ts.CreateHostUser(ctx, "admin1")
require.NoError(t, err)
adminCtx := ts.CreateUserContext(ctx, admin.ID)
first, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{})
require.NoError(t, err)
second, err := ts.Service.GetInstanceStats(adminCtx, &v1pb.GetInstanceStatsRequest{})
require.NoError(t, err)
// Cache hit: same pointer (the cache returns the stored *InstanceStats directly).
require.Same(t, first, second)
}

@ -42,6 +42,9 @@ type APIV1Service struct {
// thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion
thumbnailSemaphore *semaphore.Weighted
imageProcessingSemaphore *semaphore.Weighted
// instanceStatsCache memoizes GetInstanceStats results for instanceStatsCacheTTL.
instanceStatsCache instanceStatsCache
}
func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service {

@ -72,7 +72,7 @@ func TestFrontendService_SitemapXML(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
require.Contains(t, rec.Header().Get("Content-Type"), "application/xml")
require.Contains(t, rec.Body.String(), `<loc>https://demo.usememos.com/m/publicmemo</loc>`)
require.Contains(t, rec.Body.String(), `<loc>https://demo.usememos.com/memos/publicmemo</loc>`)
require.NotContains(t, rec.Body.String(), "privatememo")
}

@ -177,7 +177,7 @@ func (s *Store) DeleteAttachments(ctx context.Context, attachments []*Attachment
if instanceStorageSettingErr != nil && AttachmentNeedsInstanceStorageSetting(attachment) {
err = instanceStorageSettingErr
} else {
err = s.deleteAttachmentStorage(ctx, attachment, instanceStorageSetting)
err = s.deleteAttachmentStorageImpl(ctx, attachment, instanceStorageSetting)
}
if err != nil {
if attachment.StorageType == storepb.AttachmentStorageType_LOCAL {
@ -191,15 +191,15 @@ func (s *Store) DeleteAttachments(ctx context.Context, attachments []*Attachment
}
func (s *Store) DeleteAttachmentStorage(ctx context.Context, attachment *Attachment) error {
return s.deleteAttachmentStorage(ctx, attachment, nil)
return s.deleteAttachmentStorageImpl(ctx, attachment, nil)
}
// DeleteAttachmentStorageWithInstanceSetting deletes attachment storage using a preloaded instance storage setting.
func (s *Store) DeleteAttachmentStorageWithInstanceSetting(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error {
return s.deleteAttachmentStorage(ctx, attachment, instanceStorageSetting)
return s.deleteAttachmentStorageImpl(ctx, attachment, instanceStorageSetting)
}
func (s *Store) deleteAttachmentStorage(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error {
func (s *Store) deleteAttachmentStorageImpl(ctx context.Context, attachment *Attachment, instanceStorageSetting *storepb.InstanceStorageSetting) error {
if attachment == nil {
return nil
}

@ -57,6 +57,18 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
return exists, nil
}
// GetDatabaseSize returns the database size in bytes, or -1 if unavailable.
func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) {
var size int64
const q = `SELECT COALESCE(SUM(data_length + index_length), 0)
FROM information_schema.tables
WHERE table_schema = DATABASE()`
if err := d.db.QueryRowContext(ctx, q).Scan(&size); err != nil {
return -1, errors.Wrap(err, "failed to query mysql database size")
}
return size, nil
}
func mergeDSN(baseDSN string) (string, error) {
config, err := mysql.ParseDSN(baseDSN)
if err != nil {

@ -55,3 +55,13 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
}
return exists, nil
}
// GetDatabaseSize returns the database size in bytes, or -1 if unavailable.
func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) {
var size int64
const q = `SELECT pg_database_size(current_database())`
if err := d.db.QueryRowContext(ctx, q).Scan(&size); err != nil {
return -1, errors.Wrap(err, "failed to query postgres database size")
}
return size, nil
}

@ -73,3 +73,15 @@ func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
}
return exists, nil
}
// GetDatabaseSize returns the database size in bytes, or -1 if unavailable.
func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) {
var pageCount, pageSize int64
if err := d.db.QueryRowContext(ctx, "PRAGMA page_count").Scan(&pageCount); err != nil {
return -1, errors.Wrap(err, "failed to read page_count")
}
if err := d.db.QueryRowContext(ctx, "PRAGMA page_size").Scan(&pageSize); err != nil {
return -1, errors.Wrap(err, "failed to read page_size")
}
return pageCount * pageSize, nil
}

@ -13,6 +13,11 @@ type Driver interface {
IsInitialized(ctx context.Context) (bool, error)
// GetDatabaseSize returns the database size in bytes, or -1 if unavailable.
// A non-nil error indicates a hard failure; -1 with nil error means the
// driver cannot report a size from the underlying database.
GetDatabaseSize(ctx context.Context) (int64, error)
// Attachment model related methods.
CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error)
ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error)

@ -0,0 +1,19 @@
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestGetDatabaseSize(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
size, err := ts.GetDriver().GetDatabaseSize(ctx)
require.NoError(t, err)
require.Greater(t, size, int64(0), "expected database size > 0")
}

@ -0,0 +1,104 @@
import { useQueryClient } from "@tanstack/react-query";
import { RefreshCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { instanceKeys, useInstanceStats } from "@/hooks/useInstanceQueries";
import { useTranslate } from "@/utils/i18n";
import SettingGroup from "./SettingGroup";
import { SettingList, SettingListItem, SettingPanel } from "./SettingList";
import SettingSection from "./SettingSection";
const formatBytes = (bytes: number | bigint): string => {
const n = typeof bytes === "bigint" ? Number(bytes) : bytes;
if (n < 0) return "—";
if (n === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024)));
return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
};
const formatRelativeTime = (date: Date): string => {
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
return `${hours}h ago`;
};
const renderBytes = (value: bigint | number | undefined, unknown: string): string => {
if (value === undefined) return unknown;
const n = typeof value === "bigint" ? Number(value) : value;
if (n < 0) return unknown;
return formatBytes(n);
};
const StatValue = ({ value }: { value: string }) => (
<span className="block min-w-0 max-w-full break-all text-right font-mono text-sm tabular-nums text-foreground">{value}</span>
);
const StatRow = ({ label, value }: { label: string; value: string }) => (
<SettingListItem label={label} controlClassName="w-full justify-end sm:w-auto">
<StatValue value={value} />
</SettingListItem>
);
const ResourceStatsSection = () => {
const t = useTranslate();
const queryClient = useQueryClient();
const { data, isLoading, isError, isFetching } = useInstanceStats();
const unknown = t("setting.resource-stats.unknown");
const generatedTime = data?.generatedTime
? t("setting.resource-stats.last-updated", {
ago: formatRelativeTime(new Date(Number(data.generatedTime.seconds) * 1000)),
})
: undefined;
return (
<SettingSection
title={t("setting.resource-stats.title")}
description={t("setting.resource-stats.description")}
actions={
<>
{generatedTime ? <span className="text-xs text-muted-foreground">{generatedTime}</span> : null}
<Button
variant="outline"
size="sm"
disabled={isFetching}
onClick={() => void queryClient.invalidateQueries({ queryKey: instanceKeys.stats() })}
>
<RefreshCwIcon className="mr-1 size-4" />
{t("setting.resource-stats.refresh")}
</Button>
</>
}
>
{isError ? <div className="text-destructive text-sm">{t("setting.resource-stats.load-error")}</div> : null}
{isLoading && !data ? (
<SettingPanel>
<div className="px-3 py-3 text-sm text-muted-foreground"></div>
</SettingPanel>
) : null}
{data ? (
<>
<SettingGroup title={t("setting.resource-stats.database.title")}>
<SettingList>
<StatRow label={t("setting.resource-stats.database.driver")} value={data.database?.driver || unknown} />
<StatRow label={t("setting.resource-stats.database.size")} value={renderBytes(data.database?.sizeBytes, unknown)} />
</SettingList>
</SettingGroup>
<SettingGroup title={t("setting.resource-stats.local-storage.title")} showSeparator>
<SettingList>
<StatRow label={t("setting.resource-stats.local-storage.size")} value={renderBytes(data.localStorageBytes, unknown)} />
</SettingList>
</SettingGroup>
</>
) : null}
</SettingSection>
);
};
export default ResourceStatsSection;

@ -1,4 +1,5 @@
import {
BarChart3Icon,
CogIcon,
DatabaseIcon,
HeartHandshakeIcon,
@ -20,6 +21,7 @@ import MemoRelatedSettings from "@/components/Settings/MemoRelatedSettings";
import MyAccountSection from "@/components/Settings/MyAccountSection";
import NotificationSection from "@/components/Settings/NotificationSection";
import PreferencesSection from "@/components/Settings/PreferencesSection";
import ResourceStatsSection from "@/components/Settings/ResourceStatsSection";
import SSOSection from "@/components/Settings/SSOSection";
import StorageSection from "@/components/Settings/StorageSection";
import TagsSection from "@/components/Settings/TagsSection";
@ -37,7 +39,8 @@ export type SettingSectionKey =
| "notification"
| "sso"
| "tags"
| "ai";
| "ai"
| "resource-stats";
type SettingSectionScope = "basic" | "admin";
@ -132,6 +135,13 @@ export const SETTINGS_SECTIONS: SettingSectionDefinition[] = [
component: AISection,
preloadSettingKeys: [InstanceSetting_Key.AI],
},
{
key: "resource-stats",
scope: "admin",
labelKey: "setting.resource-stats.label",
icon: BarChart3Icon,
component: ResourceStatsSection,
},
];
export const DEFAULT_SETTING_SECTION: SettingSectionKey = "my-account";

@ -8,6 +8,7 @@ export const instanceKeys = {
profile: () => [...instanceKeys.all, "profile"] as const,
settings: () => [...instanceKeys.all, "settings"] as const,
setting: (key: InstanceSetting_Key) => [...instanceKeys.settings(), key] as const,
stats: () => [...instanceKeys.all, "stats"] as const,
};
// Build setting name from key
@ -16,6 +17,15 @@ const buildInstanceSettingName = (key: InstanceSetting_Key): string => {
return `instance/settings/${keyName}`;
};
// Hook to fetch instance resource statistics. Admin only on the server side.
export function useInstanceStats() {
return useQuery({
queryKey: instanceKeys.stats(),
queryFn: () => instanceServiceClient.getInstanceStats({}),
staleTime: 60_000, // 60s — matches server-side cache TTL
});
}
// Hook to fetch instance profile
export function useInstanceProfile() {
return useQuery({

@ -744,6 +744,24 @@
"invalid-regex": "Invalid or unsafe regex pattern.",
"used-count": "{{count}} memos",
"using-default-color": "Using default color."
},
"resource-stats": {
"label": "Resources",
"title": "Resource Statistics",
"description": "Resource usage for this instance.",
"database": {
"title": "Database",
"driver": "Driver",
"size": "Size"
},
"local-storage": {
"title": "Local Storage",
"size": "Data directory size"
},
"unknown": "Unknown",
"last-updated": "Last updated {{ago}}",
"refresh": "Refresh",
"load-error": "Failed to load statistics"
}
},
"tag": {

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save