更新首页

This commit is contained in:
cheney 2026-08-06 16:41:47 +08:00
parent 547181d333
commit 4d02439ecb
32 changed files with 5470 additions and 93 deletions

5
.gitignore vendored
View File

@ -31,6 +31,7 @@ lerna-debug.log*
*.swo *.swo
*~ *~
dist/* dist/*
# local env (contains API keys) # local env (contains API keys)
/.env /.env
.reasonix/*

View File

@ -52,6 +52,21 @@ export type News = Prisma.NewsModel
* *
*/ */
export type Alert = Prisma.AlertModel export type Alert = Prisma.AlertModel
/**
* Model ServerNode
*
*/
export type ServerNode = Prisma.ServerNodeModel
/**
* Model ServerNodeHistory
*
*/
export type ServerNodeHistory = Prisma.ServerNodeHistoryModel
/**
* Model ServiceCheck
*
*/
export type ServiceCheck = Prisma.ServiceCheckModel
/** /**
* Model Econ_SowInventory * Model Econ_SowInventory
* *

View File

@ -76,6 +76,21 @@ export type News = Prisma.NewsModel
* *
*/ */
export type Alert = Prisma.AlertModel export type Alert = Prisma.AlertModel
/**
* Model ServerNode
*
*/
export type ServerNode = Prisma.ServerNodeModel
/**
* Model ServerNodeHistory
*
*/
export type ServerNodeHistory = Prisma.ServerNodeHistoryModel
/**
* Model ServiceCheck
*
*/
export type ServiceCheck = Prisma.ServiceCheckModel
/** /**
* Model Econ_SowInventory * Model Econ_SowInventory
* *

View File

@ -205,6 +205,56 @@ export type StringNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
} }
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type BigIntFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
not?: Prisma.NestedBigIntFilter<$PrismaModel> | bigint | number
}
export type BigIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
not?: Prisma.NestedBigIntWithAggregatesFilter<$PrismaModel> | bigint | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedBigIntFilter<$PrismaModel>
_min?: Prisma.NestedBigIntFilter<$PrismaModel>
_max?: Prisma.NestedBigIntFilter<$PrismaModel>
}
export type JsonNullableFilter<$PrismaModel = never> = export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined< | Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>, Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
@ -229,29 +279,6 @@ export type JsonNullableFilterBase<$PrismaModel = never> = {
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
} }
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> = export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined< | Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>, Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
@ -472,6 +499,33 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
} }
export type NestedBigIntFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
not?: Prisma.NestedBigIntFilter<$PrismaModel> | bigint | number
}
export type NestedBigIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
not?: Prisma.NestedBigIntWithAggregatesFilter<$PrismaModel> | bigint | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedBigIntFilter<$PrismaModel>
_min?: Prisma.NestedBigIntFilter<$PrismaModel>
_max?: Prisma.NestedBigIntFilter<$PrismaModel>
}
export type NestedJsonNullableFilter<$PrismaModel = never> = export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined< | Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>, Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,

File diff suppressed because one or more lines are too long

View File

@ -391,6 +391,9 @@ export const ModelName = {
IndicatorPoint: 'IndicatorPoint', IndicatorPoint: 'IndicatorPoint',
News: 'News', News: 'News',
Alert: 'Alert', Alert: 'Alert',
ServerNode: 'ServerNode',
ServerNodeHistory: 'ServerNodeHistory',
ServiceCheck: 'ServiceCheck',
Econ_SowInventory: 'Econ_SowInventory', Econ_SowInventory: 'Econ_SowInventory',
ScraperCache: 'ScraperCache' ScraperCache: 'ScraperCache'
} as const } as const
@ -408,7 +411,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { meta: {
modelProps: "user" | "article" | "strategy" | "indicator" | "indicatorPoint" | "news" | "alert" | "econ_SowInventory" | "scraperCache" modelProps: "user" | "article" | "strategy" | "indicator" | "indicatorPoint" | "news" | "alert" | "serverNode" | "serverNodeHistory" | "serviceCheck" | "econ_SowInventory" | "scraperCache"
txIsolationLevel: TransactionIsolationLevel txIsolationLevel: TransactionIsolationLevel
} }
model: { model: {
@ -874,6 +877,204 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
} }
} }
} }
ServerNode: {
payload: Prisma.$ServerNodePayload<ExtArgs>
fields: Prisma.ServerNodeFieldRefs
operations: {
findUnique: {
args: Prisma.ServerNodeFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload> | null
}
findUniqueOrThrow: {
args: Prisma.ServerNodeFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
findFirst: {
args: Prisma.ServerNodeFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload> | null
}
findFirstOrThrow: {
args: Prisma.ServerNodeFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
findMany: {
args: Prisma.ServerNodeFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>[]
}
create: {
args: Prisma.ServerNodeCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
createMany: {
args: Prisma.ServerNodeCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.ServerNodeDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
update: {
args: Prisma.ServerNodeUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
deleteMany: {
args: Prisma.ServerNodeDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ServerNodeUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.ServerNodeUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
}
aggregate: {
args: Prisma.ServerNodeAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateServerNode>
}
groupBy: {
args: Prisma.ServerNodeGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServerNodeGroupByOutputType>[]
}
count: {
args: Prisma.ServerNodeCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServerNodeCountAggregateOutputType> | number
}
}
}
ServerNodeHistory: {
payload: Prisma.$ServerNodeHistoryPayload<ExtArgs>
fields: Prisma.ServerNodeHistoryFieldRefs
operations: {
findUnique: {
args: Prisma.ServerNodeHistoryFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ServerNodeHistoryFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
findFirst: {
args: Prisma.ServerNodeHistoryFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload> | null
}
findFirstOrThrow: {
args: Prisma.ServerNodeHistoryFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
findMany: {
args: Prisma.ServerNodeHistoryFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>[]
}
create: {
args: Prisma.ServerNodeHistoryCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
createMany: {
args: Prisma.ServerNodeHistoryCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.ServerNodeHistoryDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
update: {
args: Prisma.ServerNodeHistoryUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
deleteMany: {
args: Prisma.ServerNodeHistoryDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ServerNodeHistoryUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.ServerNodeHistoryUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
}
aggregate: {
args: Prisma.ServerNodeHistoryAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateServerNodeHistory>
}
groupBy: {
args: Prisma.ServerNodeHistoryGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServerNodeHistoryGroupByOutputType>[]
}
count: {
args: Prisma.ServerNodeHistoryCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServerNodeHistoryCountAggregateOutputType> | number
}
}
}
ServiceCheck: {
payload: Prisma.$ServiceCheckPayload<ExtArgs>
fields: Prisma.ServiceCheckFieldRefs
operations: {
findUnique: {
args: Prisma.ServiceCheckFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ServiceCheckFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
findFirst: {
args: Prisma.ServiceCheckFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload> | null
}
findFirstOrThrow: {
args: Prisma.ServiceCheckFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
findMany: {
args: Prisma.ServiceCheckFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>[]
}
create: {
args: Prisma.ServiceCheckCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
createMany: {
args: Prisma.ServiceCheckCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.ServiceCheckDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
update: {
args: Prisma.ServiceCheckUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
deleteMany: {
args: Prisma.ServiceCheckDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ServiceCheckUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.ServiceCheckUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
}
aggregate: {
args: Prisma.ServiceCheckAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateServiceCheck>
}
groupBy: {
args: Prisma.ServiceCheckGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServiceCheckGroupByOutputType>[]
}
count: {
args: Prisma.ServiceCheckCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ServiceCheckCountAggregateOutputType> | number
}
}
}
Econ_SowInventory: { Econ_SowInventory: {
payload: Prisma.$Econ_SowInventoryPayload<ExtArgs> payload: Prisma.$Econ_SowInventoryPayload<ExtArgs>
fields: Prisma.Econ_SowInventoryFieldRefs fields: Prisma.Econ_SowInventoryFieldRefs
@ -1114,6 +1315,7 @@ export const NewsScalarFieldEnum = {
id: 'id', id: 'id',
title: 'title', title: 'title',
summary: 'summary', summary: 'summary',
content: 'content',
source: 'source', source: 'source',
url: 'url', url: 'url',
publishedAt: 'publishedAt', publishedAt: 'publishedAt',
@ -1140,6 +1342,70 @@ export const AlertScalarFieldEnum = {
export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum] export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum]
export const ServerNodeScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
name: 'name',
cpuName: 'cpuName',
os: 'os',
region: 'region',
memTotal: 'memTotal',
diskTotal: 'diskTotal',
cpuCores: 'cpuCores',
cpuUsage: 'cpuUsage',
ramUsed: 'ramUsed',
swapUsed: 'swapUsed',
load1: 'load1',
load5: 'load5',
load15: 'load15',
diskUsed: 'diskUsed',
netIn: 'netIn',
netOut: 'netOut',
uptime: 'uptime',
process: 'process',
connections: 'connections',
status: 'status',
recordedAt: 'recordedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServerNodeScalarFieldEnum = (typeof ServerNodeScalarFieldEnum)[keyof typeof ServerNodeScalarFieldEnum]
export const ServerNodeHistoryScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
period: 'period',
cpu: 'cpu',
ram: 'ram',
load: 'load',
disk: 'disk',
netIn: 'netIn',
netOut: 'netOut',
process: 'process',
connections: 'connections',
createdAt: 'createdAt'
} as const
export type ServerNodeHistoryScalarFieldEnum = (typeof ServerNodeHistoryScalarFieldEnum)[keyof typeof ServerNodeHistoryScalarFieldEnum]
export const ServiceCheckScalarFieldEnum = {
id: 'id',
name: 'name',
url: 'url',
status: 'status',
latencyMs: 'latencyMs',
message: 'message',
lastCheckAt: 'lastCheckAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceCheckScalarFieldEnum = (typeof ServiceCheckScalarFieldEnum)[keyof typeof ServiceCheckScalarFieldEnum]
export const Econ_SowInventoryScalarFieldEnum = { export const Econ_SowInventoryScalarFieldEnum = {
id: 'id', id: 'id',
month: 'month', month: 'month',
@ -1250,9 +1516,18 @@ export const IndicatorPointOrderByRelevanceFieldEnum = {
export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum] export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const NewsOrderByRelevanceFieldEnum = { export const NewsOrderByRelevanceFieldEnum = {
title: 'title', title: 'title',
summary: 'summary', summary: 'summary',
content: 'content',
source: 'source', source: 'source',
url: 'url' url: 'url'
} as const } as const
@ -1270,6 +1545,35 @@ export const AlertOrderByRelevanceFieldEnum = {
export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum] export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum]
export const ServerNodeOrderByRelevanceFieldEnum = {
uuid: 'uuid',
name: 'name',
cpuName: 'cpuName',
os: 'os',
region: 'region',
status: 'status'
} as const
export type ServerNodeOrderByRelevanceFieldEnum = (typeof ServerNodeOrderByRelevanceFieldEnum)[keyof typeof ServerNodeOrderByRelevanceFieldEnum]
export const ServerNodeHistoryOrderByRelevanceFieldEnum = {
uuid: 'uuid'
} as const
export type ServerNodeHistoryOrderByRelevanceFieldEnum = (typeof ServerNodeHistoryOrderByRelevanceFieldEnum)[keyof typeof ServerNodeHistoryOrderByRelevanceFieldEnum]
export const ServiceCheckOrderByRelevanceFieldEnum = {
name: 'name',
url: 'url',
status: 'status',
message: 'message'
} as const
export type ServiceCheckOrderByRelevanceFieldEnum = (typeof ServiceCheckOrderByRelevanceFieldEnum)[keyof typeof ServiceCheckOrderByRelevanceFieldEnum]
export const Econ_SowInventoryOrderByRelevanceFieldEnum = { export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
month: 'month' month: 'month'
} as const } as const
@ -1277,14 +1581,6 @@ export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum] export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const ScraperCacheOrderByRelevanceFieldEnum = { export const ScraperCacheOrderByRelevanceFieldEnum = {
scraperName: 'scraperName', scraperName: 'scraperName',
lastDataHash: 'lastDataHash' lastDataHash: 'lastDataHash'
@ -1347,6 +1643,13 @@ export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel,
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'> export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'BigInt'
*/
export type BigIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BigInt'>
/** /**
* Batch Payload for updateMany & deleteMany & createMany * Batch Payload for updateMany & deleteMany & createMany
*/ */
@ -1449,6 +1752,9 @@ export type GlobalOmitConfig = {
indicatorPoint?: Prisma.IndicatorPointOmit indicatorPoint?: Prisma.IndicatorPointOmit
news?: Prisma.NewsOmit news?: Prisma.NewsOmit
alert?: Prisma.AlertOmit alert?: Prisma.AlertOmit
serverNode?: Prisma.ServerNodeOmit
serverNodeHistory?: Prisma.ServerNodeHistoryOmit
serviceCheck?: Prisma.ServiceCheckOmit
econ_SowInventory?: Prisma.Econ_SowInventoryOmit econ_SowInventory?: Prisma.Econ_SowInventoryOmit
scraperCache?: Prisma.ScraperCacheOmit scraperCache?: Prisma.ScraperCacheOmit
} }

View File

@ -58,6 +58,9 @@ export const ModelName = {
IndicatorPoint: 'IndicatorPoint', IndicatorPoint: 'IndicatorPoint',
News: 'News', News: 'News',
Alert: 'Alert', Alert: 'Alert',
ServerNode: 'ServerNode',
ServerNodeHistory: 'ServerNodeHistory',
ServiceCheck: 'ServiceCheck',
Econ_SowInventory: 'Econ_SowInventory', Econ_SowInventory: 'Econ_SowInventory',
ScraperCache: 'ScraperCache' ScraperCache: 'ScraperCache'
} as const } as const
@ -147,6 +150,7 @@ export const NewsScalarFieldEnum = {
id: 'id', id: 'id',
title: 'title', title: 'title',
summary: 'summary', summary: 'summary',
content: 'content',
source: 'source', source: 'source',
url: 'url', url: 'url',
publishedAt: 'publishedAt', publishedAt: 'publishedAt',
@ -173,6 +177,70 @@ export const AlertScalarFieldEnum = {
export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum] export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum]
export const ServerNodeScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
name: 'name',
cpuName: 'cpuName',
os: 'os',
region: 'region',
memTotal: 'memTotal',
diskTotal: 'diskTotal',
cpuCores: 'cpuCores',
cpuUsage: 'cpuUsage',
ramUsed: 'ramUsed',
swapUsed: 'swapUsed',
load1: 'load1',
load5: 'load5',
load15: 'load15',
diskUsed: 'diskUsed',
netIn: 'netIn',
netOut: 'netOut',
uptime: 'uptime',
process: 'process',
connections: 'connections',
status: 'status',
recordedAt: 'recordedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServerNodeScalarFieldEnum = (typeof ServerNodeScalarFieldEnum)[keyof typeof ServerNodeScalarFieldEnum]
export const ServerNodeHistoryScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
period: 'period',
cpu: 'cpu',
ram: 'ram',
load: 'load',
disk: 'disk',
netIn: 'netIn',
netOut: 'netOut',
process: 'process',
connections: 'connections',
createdAt: 'createdAt'
} as const
export type ServerNodeHistoryScalarFieldEnum = (typeof ServerNodeHistoryScalarFieldEnum)[keyof typeof ServerNodeHistoryScalarFieldEnum]
export const ServiceCheckScalarFieldEnum = {
id: 'id',
name: 'name',
url: 'url',
status: 'status',
latencyMs: 'latencyMs',
message: 'message',
lastCheckAt: 'lastCheckAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ServiceCheckScalarFieldEnum = (typeof ServiceCheckScalarFieldEnum)[keyof typeof ServiceCheckScalarFieldEnum]
export const Econ_SowInventoryScalarFieldEnum = { export const Econ_SowInventoryScalarFieldEnum = {
id: 'id', id: 'id',
month: 'month', month: 'month',
@ -283,9 +351,18 @@ export const IndicatorPointOrderByRelevanceFieldEnum = {
export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum] export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const NewsOrderByRelevanceFieldEnum = { export const NewsOrderByRelevanceFieldEnum = {
title: 'title', title: 'title',
summary: 'summary', summary: 'summary',
content: 'content',
source: 'source', source: 'source',
url: 'url' url: 'url'
} as const } as const
@ -303,6 +380,35 @@ export const AlertOrderByRelevanceFieldEnum = {
export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum] export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum]
export const ServerNodeOrderByRelevanceFieldEnum = {
uuid: 'uuid',
name: 'name',
cpuName: 'cpuName',
os: 'os',
region: 'region',
status: 'status'
} as const
export type ServerNodeOrderByRelevanceFieldEnum = (typeof ServerNodeOrderByRelevanceFieldEnum)[keyof typeof ServerNodeOrderByRelevanceFieldEnum]
export const ServerNodeHistoryOrderByRelevanceFieldEnum = {
uuid: 'uuid'
} as const
export type ServerNodeHistoryOrderByRelevanceFieldEnum = (typeof ServerNodeHistoryOrderByRelevanceFieldEnum)[keyof typeof ServerNodeHistoryOrderByRelevanceFieldEnum]
export const ServiceCheckOrderByRelevanceFieldEnum = {
name: 'name',
url: 'url',
status: 'status',
message: 'message'
} as const
export type ServiceCheckOrderByRelevanceFieldEnum = (typeof ServiceCheckOrderByRelevanceFieldEnum)[keyof typeof ServiceCheckOrderByRelevanceFieldEnum]
export const Econ_SowInventoryOrderByRelevanceFieldEnum = { export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
month: 'month' month: 'month'
} as const } as const
@ -310,14 +416,6 @@ export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum] export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const ScraperCacheOrderByRelevanceFieldEnum = { export const ScraperCacheOrderByRelevanceFieldEnum = {
scraperName: 'scraperName', scraperName: 'scraperName',
lastDataHash: 'lastDataHash' lastDataHash: 'lastDataHash'

View File

@ -15,6 +15,9 @@ export type * from './models/Indicator.ts'
export type * from './models/IndicatorPoint.ts' export type * from './models/IndicatorPoint.ts'
export type * from './models/News.ts' export type * from './models/News.ts'
export type * from './models/Alert.ts' export type * from './models/Alert.ts'
export type * from './models/ServerNode.ts'
export type * from './models/ServerNodeHistory.ts'
export type * from './models/ServiceCheck.ts'
export type * from './models/Econ_SowInventory.ts' export type * from './models/Econ_SowInventory.ts'
export type * from './models/ScraperCache.ts' export type * from './models/ScraperCache.ts'
export type * from './commonInputTypes.ts' export type * from './commonInputTypes.ts'

View File

@ -40,6 +40,7 @@ export type NewsMinAggregateOutputType = {
id: number | null id: number | null
title: string | null title: string | null
summary: string | null summary: string | null
content: string | null
source: string | null source: string | null
url: string | null url: string | null
publishedAt: Date | null publishedAt: Date | null
@ -51,6 +52,7 @@ export type NewsMaxAggregateOutputType = {
id: number | null id: number | null
title: string | null title: string | null
summary: string | null summary: string | null
content: string | null
source: string | null source: string | null
url: string | null url: string | null
publishedAt: Date | null publishedAt: Date | null
@ -62,6 +64,7 @@ export type NewsCountAggregateOutputType = {
id: number id: number
title: number title: number
summary: number summary: number
content: number
source: number source: number
url: number url: number
publishedAt: number publishedAt: number
@ -85,6 +88,7 @@ export type NewsMinAggregateInputType = {
id?: true id?: true
title?: true title?: true
summary?: true summary?: true
content?: true
source?: true source?: true
url?: true url?: true
publishedAt?: true publishedAt?: true
@ -96,6 +100,7 @@ export type NewsMaxAggregateInputType = {
id?: true id?: true
title?: true title?: true
summary?: true summary?: true
content?: true
source?: true source?: true
url?: true url?: true
publishedAt?: true publishedAt?: true
@ -107,6 +112,7 @@ export type NewsCountAggregateInputType = {
id?: true id?: true
title?: true title?: true
summary?: true summary?: true
content?: true
source?: true source?: true
url?: true url?: true
publishedAt?: true publishedAt?: true
@ -205,6 +211,7 @@ export type NewsGroupByOutputType = {
id: number id: number
title: string title: string
summary: string summary: string
content: string | null
source: string source: string
url: string url: string
publishedAt: Date publishedAt: Date
@ -239,6 +246,7 @@ export type NewsWhereInput = {
id?: Prisma.IntFilter<"News"> | number id?: Prisma.IntFilter<"News"> | number
title?: Prisma.StringFilter<"News"> | string title?: Prisma.StringFilter<"News"> | string
summary?: Prisma.StringFilter<"News"> | string summary?: Prisma.StringFilter<"News"> | string
content?: Prisma.StringNullableFilter<"News"> | string | null
source?: Prisma.StringFilter<"News"> | string source?: Prisma.StringFilter<"News"> | string
url?: Prisma.StringFilter<"News"> | string url?: Prisma.StringFilter<"News"> | string
publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string
@ -250,6 +258,7 @@ export type NewsOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
title?: Prisma.SortOrder title?: Prisma.SortOrder
summary?: Prisma.SortOrder summary?: Prisma.SortOrder
content?: Prisma.SortOrderInput | Prisma.SortOrder
source?: Prisma.SortOrder source?: Prisma.SortOrder
url?: Prisma.SortOrder url?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder publishedAt?: Prisma.SortOrder
@ -266,6 +275,7 @@ export type NewsWhereUniqueInput = Prisma.AtLeast<{
NOT?: Prisma.NewsWhereInput | Prisma.NewsWhereInput[] NOT?: Prisma.NewsWhereInput | Prisma.NewsWhereInput[]
title?: Prisma.StringFilter<"News"> | string title?: Prisma.StringFilter<"News"> | string
summary?: Prisma.StringFilter<"News"> | string summary?: Prisma.StringFilter<"News"> | string
content?: Prisma.StringNullableFilter<"News"> | string | null
source?: Prisma.StringFilter<"News"> | string source?: Prisma.StringFilter<"News"> | string
url?: Prisma.StringFilter<"News"> | string url?: Prisma.StringFilter<"News"> | string
publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string
@ -277,6 +287,7 @@ export type NewsOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
title?: Prisma.SortOrder title?: Prisma.SortOrder
summary?: Prisma.SortOrder summary?: Prisma.SortOrder
content?: Prisma.SortOrderInput | Prisma.SortOrder
source?: Prisma.SortOrder source?: Prisma.SortOrder
url?: Prisma.SortOrder url?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder publishedAt?: Prisma.SortOrder
@ -296,6 +307,7 @@ export type NewsScalarWhereWithAggregatesInput = {
id?: Prisma.IntWithAggregatesFilter<"News"> | number id?: Prisma.IntWithAggregatesFilter<"News"> | number
title?: Prisma.StringWithAggregatesFilter<"News"> | string title?: Prisma.StringWithAggregatesFilter<"News"> | string
summary?: Prisma.StringWithAggregatesFilter<"News"> | string summary?: Prisma.StringWithAggregatesFilter<"News"> | string
content?: Prisma.StringNullableWithAggregatesFilter<"News"> | string | null
source?: Prisma.StringWithAggregatesFilter<"News"> | string source?: Prisma.StringWithAggregatesFilter<"News"> | string
url?: Prisma.StringWithAggregatesFilter<"News"> | string url?: Prisma.StringWithAggregatesFilter<"News"> | string
publishedAt?: Prisma.DateTimeWithAggregatesFilter<"News"> | Date | string publishedAt?: Prisma.DateTimeWithAggregatesFilter<"News"> | Date | string
@ -306,6 +318,7 @@ export type NewsScalarWhereWithAggregatesInput = {
export type NewsCreateInput = { export type NewsCreateInput = {
title: string title: string
summary?: string summary?: string
content?: string | null
source?: string source?: string
url?: string url?: string
publishedAt: Date | string publishedAt: Date | string
@ -317,6 +330,7 @@ export type NewsUncheckedCreateInput = {
id?: number id?: number
title: string title: string
summary?: string summary?: string
content?: string | null
source?: string source?: string
url?: string url?: string
publishedAt: Date | string publishedAt: Date | string
@ -327,6 +341,7 @@ export type NewsUncheckedCreateInput = {
export type NewsUpdateInput = { export type NewsUpdateInput = {
title?: Prisma.StringFieldUpdateOperationsInput | string title?: Prisma.StringFieldUpdateOperationsInput | string
summary?: Prisma.StringFieldUpdateOperationsInput | string summary?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
source?: Prisma.StringFieldUpdateOperationsInput | string source?: Prisma.StringFieldUpdateOperationsInput | string
url?: Prisma.StringFieldUpdateOperationsInput | string url?: Prisma.StringFieldUpdateOperationsInput | string
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -338,6 +353,7 @@ export type NewsUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
title?: Prisma.StringFieldUpdateOperationsInput | string title?: Prisma.StringFieldUpdateOperationsInput | string
summary?: Prisma.StringFieldUpdateOperationsInput | string summary?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
source?: Prisma.StringFieldUpdateOperationsInput | string source?: Prisma.StringFieldUpdateOperationsInput | string
url?: Prisma.StringFieldUpdateOperationsInput | string url?: Prisma.StringFieldUpdateOperationsInput | string
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -349,6 +365,7 @@ export type NewsCreateManyInput = {
id?: number id?: number
title: string title: string
summary?: string summary?: string
content?: string | null
source?: string source?: string
url?: string url?: string
publishedAt: Date | string publishedAt: Date | string
@ -359,6 +376,7 @@ export type NewsCreateManyInput = {
export type NewsUpdateManyMutationInput = { export type NewsUpdateManyMutationInput = {
title?: Prisma.StringFieldUpdateOperationsInput | string title?: Prisma.StringFieldUpdateOperationsInput | string
summary?: Prisma.StringFieldUpdateOperationsInput | string summary?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
source?: Prisma.StringFieldUpdateOperationsInput | string source?: Prisma.StringFieldUpdateOperationsInput | string
url?: Prisma.StringFieldUpdateOperationsInput | string url?: Prisma.StringFieldUpdateOperationsInput | string
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -370,6 +388,7 @@ export type NewsUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
title?: Prisma.StringFieldUpdateOperationsInput | string title?: Prisma.StringFieldUpdateOperationsInput | string
summary?: Prisma.StringFieldUpdateOperationsInput | string summary?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
source?: Prisma.StringFieldUpdateOperationsInput | string source?: Prisma.StringFieldUpdateOperationsInput | string
url?: Prisma.StringFieldUpdateOperationsInput | string url?: Prisma.StringFieldUpdateOperationsInput | string
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@ -392,6 +411,7 @@ export type NewsCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
title?: Prisma.SortOrder title?: Prisma.SortOrder
summary?: Prisma.SortOrder summary?: Prisma.SortOrder
content?: Prisma.SortOrder
source?: Prisma.SortOrder source?: Prisma.SortOrder
url?: Prisma.SortOrder url?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder publishedAt?: Prisma.SortOrder
@ -408,6 +428,7 @@ export type NewsMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
title?: Prisma.SortOrder title?: Prisma.SortOrder
summary?: Prisma.SortOrder summary?: Prisma.SortOrder
content?: Prisma.SortOrder
source?: Prisma.SortOrder source?: Prisma.SortOrder
url?: Prisma.SortOrder url?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder publishedAt?: Prisma.SortOrder
@ -419,6 +440,7 @@ export type NewsMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
title?: Prisma.SortOrder title?: Prisma.SortOrder
summary?: Prisma.SortOrder summary?: Prisma.SortOrder
content?: Prisma.SortOrder
source?: Prisma.SortOrder source?: Prisma.SortOrder
url?: Prisma.SortOrder url?: Prisma.SortOrder
publishedAt?: Prisma.SortOrder publishedAt?: Prisma.SortOrder
@ -431,12 +453,17 @@ export type NewsSumOrderByAggregateInput = {
impactRank?: Prisma.SortOrder impactRank?: Prisma.SortOrder
} }
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type NewsSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type NewsSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
title?: boolean title?: boolean
summary?: boolean summary?: boolean
content?: boolean
source?: boolean source?: boolean
url?: boolean url?: boolean
publishedAt?: boolean publishedAt?: boolean
@ -450,6 +477,7 @@ export type NewsSelectScalar = {
id?: boolean id?: boolean
title?: boolean title?: boolean
summary?: boolean summary?: boolean
content?: boolean
source?: boolean source?: boolean
url?: boolean url?: boolean
publishedAt?: boolean publishedAt?: boolean
@ -457,7 +485,7 @@ export type NewsSelectScalar = {
createdAt?: boolean createdAt?: boolean
} }
export type NewsOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "title" | "summary" | "source" | "url" | "publishedAt" | "impactRank" | "createdAt", ExtArgs["result"]["news"]> export type NewsOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "title" | "summary" | "content" | "source" | "url" | "publishedAt" | "impactRank" | "createdAt", ExtArgs["result"]["news"]>
export type $NewsPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $NewsPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "News" name: "News"
@ -466,6 +494,7 @@ export type $NewsPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
id: number id: number
title: string title: string
summary: string summary: string
content: string | null
source: string source: string
url: string url: string
publishedAt: Date publishedAt: Date
@ -843,6 +872,7 @@ export interface NewsFieldRefs {
readonly id: Prisma.FieldRef<"News", 'Int'> readonly id: Prisma.FieldRef<"News", 'Int'>
readonly title: Prisma.FieldRef<"News", 'String'> readonly title: Prisma.FieldRef<"News", 'String'>
readonly summary: Prisma.FieldRef<"News", 'String'> readonly summary: Prisma.FieldRef<"News", 'String'>
readonly content: Prisma.FieldRef<"News", 'String'>
readonly source: Prisma.FieldRef<"News", 'String'> readonly source: Prisma.FieldRef<"News", 'String'>
readonly url: Prisma.FieldRef<"News", 'String'> readonly url: Prisma.FieldRef<"News", 'String'>
readonly publishedAt: Prisma.FieldRef<"News", 'DateTime'> readonly publishedAt: Prisma.FieldRef<"News", 'DateTime'>

View File

@ -391,10 +391,6 @@ export type ScraperCacheSumOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
} }
export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type ScraperCacheSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type ScraperCacheSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

24
package-lock.json generated
View File

@ -24,7 +24,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"recharts": "^3.8.1" "recharts": "^3.8.1",
"ws": "^8.21.2"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",
@ -6360,6 +6361,27 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.21.2",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.2.tgz",
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",

View File

@ -41,7 +41,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"recharts": "^3.8.1" "recharts": "^3.8.1",
"ws": "^8.21.2"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",

View File

@ -0,0 +1,68 @@
-- 服务器监控 + 服务健康检查
-- CreateTable
CREATE TABLE `ServerNode` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`cpuName` VARCHAR(191) NOT NULL DEFAULT '',
`os` VARCHAR(191) NOT NULL DEFAULT '',
`region` VARCHAR(191) NOT NULL DEFAULT '',
`memTotal` BIGINT NOT NULL DEFAULT 0,
`diskTotal` BIGINT NOT NULL DEFAULT 0,
`cpuCores` INTEGER NOT NULL DEFAULT 0,
`cpuUsage` DOUBLE NOT NULL DEFAULT 0,
`ramUsed` BIGINT NOT NULL DEFAULT 0,
`swapUsed` BIGINT NOT NULL DEFAULT 0,
`load1` DOUBLE NOT NULL DEFAULT 0,
`load5` DOUBLE NOT NULL DEFAULT 0,
`load15` DOUBLE NOT NULL DEFAULT 0,
`diskUsed` BIGINT NOT NULL DEFAULT 0,
`netIn` BIGINT NOT NULL DEFAULT 0,
`netOut` BIGINT NOT NULL DEFAULT 0,
`uptime` BIGINT NOT NULL DEFAULT 0,
`process` INTEGER NOT NULL DEFAULT 0,
`connections` INTEGER NOT NULL DEFAULT 0,
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
`recordedAt` DATETIME(3) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `ServerNode_uuid_key`(`uuid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ServerNodeHistory` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(191) NOT NULL,
`period` DATETIME(3) NOT NULL,
`cpu` DOUBLE NOT NULL DEFAULT 0,
`ram` BIGINT NOT NULL DEFAULT 0,
`load` DOUBLE NOT NULL DEFAULT 0,
`disk` BIGINT NOT NULL DEFAULT 0,
`netIn` BIGINT NOT NULL DEFAULT 0,
`netOut` BIGINT NOT NULL DEFAULT 0,
`process` INTEGER NOT NULL DEFAULT 0,
`connections` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `ServerNodeHistory_uuid_period_key`(`uuid`, `period`),
INDEX `ServerNodeHistory_uuid_period_idx`(`uuid`, `period`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ServiceCheck` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`url` VARCHAR(191) NOT NULL,
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
`latencyMs` INTEGER NOT NULL DEFAULT 0,
`message` VARCHAR(191) NOT NULL DEFAULT '',
`lastCheckAt` DATETIME(3) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `ServiceCheck_name_key`(`name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -0,0 +1,2 @@
-- AlterTable (news.content 全文, 须在 news_and_alerts 建表之后执行)
ALTER TABLE `News` ADD COLUMN `content` TEXT NULL;

View File

@ -1,3 +1,3 @@
# Please do not edit this file manually # Please do not edit this file manually
# It should be added in your version-control system (e.g., Git) # It should be added in your version-control system (e.g., Git)
provider = "mysql" provider = "mysql"

View File

@ -72,6 +72,7 @@ model News {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
title String title String
summary String @default("") summary String @default("")
content String? @db.Text
source String @default("") source String @default("")
url String @default("") url String @default("")
publishedAt DateTime publishedAt DateTime
@ -98,6 +99,69 @@ model Alert {
@@index([createdAt]) @@index([createdAt])
} }
// 服务器监控: 对接 komari 公开 API
model ServerNode {
id Int @id @default(autoincrement())
uuid String @unique
name String
cpuName String @default("")
os String @default("")
region String @default("")
memTotal BigInt @default(0)
diskTotal BigInt @default(0)
cpuCores Int @default(0)
// 最新实时快照
cpuUsage Float @default(0)
ramUsed BigInt @default(0)
swapUsed BigInt @default(0)
load1 Float @default(0)
load5 Float @default(0)
load15 Float @default(0)
diskUsed BigInt @default(0)
netIn BigInt @default(0)
netOut BigInt @default(0)
uptime BigInt @default(0)
process Int @default(0)
connections Int @default(0)
status String @default("unknown") // online | offline | unknown
recordedAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// 时序历史: 每节点每分钟一条 (komari records 1 分钟粒度)
model ServerNodeHistory {
id Int @id @default(autoincrement())
uuid String
period DateTime
cpu Float @default(0)
ram BigInt @default(0)
load Float @default(0)
disk BigInt @default(0)
netIn BigInt @default(0)
netOut BigInt @default(0)
process Int @default(0)
connections Int @default(0)
createdAt DateTime @default(now())
@@unique([uuid, period])
@@index([uuid, period])
}
// 服务健康检查: 本应用自检 (健康端点 / DB / 第三方)
model ServiceCheck {
id Int @id @default(autoincrement())
name String @unique
url String
status String @default("unknown") // up | down | unknown
latencyMs Int @default(0)
message String @default("")
lastCheckAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Econ_SowInventory { model Econ_SowInventory {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
month String @unique month String @unique

72
scripts/.fix-cron.cjs Normal file
View File

@ -0,0 +1,72 @@
const fs = require('fs')
const content = `import cron from 'node-cron'
import { prisma } from './db.js'
import mysql from 'mysql2/promise'
import { fetchNewsList, persistNews } from '../scripts/fetchers/news.fetcher.js'
import { checkAfterRefresh } from './alert-engine.js'
function parseUrl(url) {
const u = new URL(url)
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\\//, '') }
}
async function openConn() {
return mysql.createConnection(parseUrl(process.env.DATABASE_URL))
}
// 每天凌晨 3:00 (本地时间) 拉取新闻 + 跑跨源对比
function scheduleNews() {
cron.schedule('0 3 * * *', async () => {
console.log('[cron] 3:00 触发')
try {
const list = await fetchNewsList()
const r = await persistNews(list)
console.log('[cron] 新闻入库: 新增 ' + r.inserted + ' / 跳过 ' + r.skipped)
const conn = await openConn()
try {
const a = await checkAfterRefresh(conn)
console.log('[cron] 报警检测: 新增 ' + a.inserted + ' 条 (扫描 ' + a.scanned + ')')
} finally { await conn.end() }
} catch (e) {
console.error('[cron] 任务失败:', e.message)
}
})
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
}
// 每 2 分钟: 拉取 komari 节点 + 服务自检
function scheduleMonitors() {
cron.schedule('*/2 * * * *', async () => {
try {
const k = require('./komari-monitor.cjs')
const r = await k.runOnce()
console.log('[cron] komari: nodes=' + r.nodes + ' snapshots=' + r.snapshots + ' history=' + r.history + ' failed=' + r.failed)
} catch (e) { console.error('[cron] komari 任务失败:', e.message) }
try {
const h = require('./health-check.cjs')
const arr = await h.runOnce()
const down = arr.filter(x => x.status !== 'up').map(x => x.name)
console.log('[cron] health: ' + arr.length + ' 项, down=' + (down.length ? down.join(',') : '无'))
} catch (e) { console.error('[cron] health 任务失败:', e.message) }
})
console.log('[cron] 已注册: 每 2 分钟 komari + 服务自检')
}
// 每小时: 清理 24h 前的 komari 历史
function scheduleMonitorCleanup() {
cron.schedule('0 * * * *', async () => {
try {
const k = require('./komari-monitor.cjs')
const n = await k.cleanupOld()
console.log('[cron] 监控历史清理: 删除 ' + n + ' 条')
} catch (e) { console.error('[cron] 监控清理失败:', e.message) }
})
console.log('[cron] 已注册: 每小时清理 24h 前监控历史')
}
scheduleNews()
scheduleMonitors()
scheduleMonitorCleanup()
`
fs.writeFileSync('server/cron.js', content, 'utf8')
console.log('OK len=' + content.length)

View File

@ -50,15 +50,17 @@ function parseNewsSheet(sheet, impactBase = 0) {
const out = [] const out = []
for (let i = 0; i < sheet.items.length; i++) { for (let i = 0; i < sheet.items.length; i++) {
const r = sheet.items[i] const r = sheet.items[i]
const title = String(r[0] || '').trim() const title = String(r[0] || '').trim().slice(0, 190)
if (!title) continue if (!title) continue
const publishedAt = parsePublishedAt(r[2]) const publishedAt = parsePublishedAt(r[2])
if (!publishedAt) continue if (!publishedAt) continue
const body = String(r[1] || '').trim()
out.push({ out.push({
title, title,
summary: String(r[1] || '').trim().slice(0, 180), summary: body.slice(0, 180),
content: body.slice(0, 8000), // 正文全文 (MCP "摘要"列实际是正文), 上限防异常超长
source: String(r[3] || '').trim().slice(0, 80), source: String(r[3] || '').trim().slice(0, 80),
url: String(r[4] || '').trim().slice(0, 400), url: String(r[4] || '').trim().slice(0, 190), // 列宽 varchar(191), 超长会被 MySQL 拒绝
publishedAt, publishedAt,
impactRank: impactBase + i + 1, impactRank: impactBase + i + 1,
}) })
@ -103,20 +105,27 @@ export async function persistNews(newsList) {
if (!process.env.DATABASE_URL) throw new Error('缺少 DATABASE_URL') if (!process.env.DATABASE_URL) throw new Error('缺少 DATABASE_URL')
if (!newsList || newsList.length === 0) return { inserted: 0, skipped: 0 } if (!newsList || newsList.length === 0) return { inserted: 0, skipped: 0 }
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL)) const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
let inserted = 0, skipped = 0 let inserted = 0, skipped = 0, failed = 0
try { try {
for (const n of newsList) { for (const n of newsList) {
const [r] = await conn.execute( try {
'INSERT INTO `News` (`title`, `summary`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' + // 注意: content 列依赖迁移 add_news_content 已应用, 发布顺序须先迁移后跑本脚本
'VALUES (?, ?, ?, ?, ?, ?, NOW(3)) ' + // 重复命中 (title, publishedAt) 时只回填仍为 NULL 的 content, 避免覆盖已有全文
'ON DUPLICATE KEY UPDATE `impactRank` = VALUES(`impactRank`)', const [r] = await conn.execute(
[n.title, n.summary, n.source, n.url, n.publishedAt, n.impactRank], 'INSERT INTO `News` (`title`, `summary`, `content`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' +
) 'VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3)) ' +
if (r.affectedRows === 1) inserted++ 'AS new ON DUPLICATE KEY UPDATE `impactRank` = new.`impactRank`, `content` = IFNULL(News.`content`, new.`content`)',
else skipped++ [n.title, n.summary, n.content || null, n.source, n.url, n.publishedAt, n.impactRank],
)
if (r.affectedRows === 1) inserted++
else skipped++
} catch (e) {
failed++
console.error('[News] 单条入库失败:', n.title, e.message)
}
} }
} finally { } finally {
await conn.end() await conn.end()
} }
return { inserted, skipped } return { inserted, skipped, failed }
} }

View File

@ -7,6 +7,6 @@ async function main() {
console.log('[News] 拉取 ' + list.length + ' 条, 耗时 ' + ((Date.now() - t0) / 1000).toFixed(1) + 's') console.log('[News] 拉取 ' + list.length + ' 条, 耗时 ' + ((Date.now() - t0) / 1000).toFixed(1) + 's')
if (list.length === 0) { console.log('[News] 无数据, 跳过'); return } if (list.length === 0) { console.log('[News] 无数据, 跳过'); return }
const r = await mod.persistNews(list) const r = await mod.persistNews(list)
console.log('[News] 入库: 新增 ' + r.inserted + ' 条, 重复跳过 ' + r.skipped + ' 条') console.log('[News] 入库: 新增 ' + r.inserted + ' 条, 重复跳过 ' + r.skipped + ' 条' + (r.failed ? ', 失败 ' + r.failed + ' 条' : ''))
} }
main().catch((e) => { console.error('[News] 失败:', e); process.exit(1) }) main().catch((e) => { console.error('[News] 失败:', e); process.exit(1) })

View File

@ -33,4 +33,36 @@ function scheduleNews() {
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测') console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
} }
// 每 2 分钟: 拉取 komari 节点 + 服务自检
function scheduleMonitors() {
cron.schedule('*/2 * * * *', async () => {
try {
const k = require('./komari-monitor.cjs')
const r = await k.runOnce()
console.log('[cron] komari: nodes=' + r.nodes + ' snapshots=' + r.snapshots + ' history=' + r.history + ' failed=' + r.failed)
} catch (e) { console.error('[cron] komari 任务失败:', e.message) }
try {
const h = require('./health-check.cjs')
const arr = await h.runOnce()
const down = arr.filter(x => x.status !== 'up').map(x => x.name)
console.log('[cron] health: ' + arr.length + ' 项, down=' + (down.length ? down.join(',') : '无'))
} catch (e) { console.error('[cron] health 任务失败:', e.message) }
})
console.log('[cron] 已注册: 每 2 分钟 komari + 服务自检')
}
// 每小时: 清理 24h 前的 komari 历史
function scheduleMonitorCleanup() {
cron.schedule('0 * * * *', async () => {
try {
const k = require('./komari-monitor.cjs')
const n = await k.cleanupOld()
console.log('[cron] 监控历史清理: 删除 ' + n + ' 条')
} catch (e) { console.error('[cron] 监控清理失败:', e.message) }
})
console.log('[cron] 已注册: 每小时清理 24h 前监控历史')
}
scheduleNews() scheduleNews()
scheduleMonitors()
scheduleMonitorCleanup()

55
server/health-check.cjs Normal file
View File

@ -0,0 +1,55 @@
// 服务健康检查: 探测本应用各关键端点, 写入 ServiceCheck 表
const mysql = require('mysql2/promise')
function parseUrl(url) { const u = new URL(url); return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') } }
const TARGETS = [
{ name: 'api-health', url: 'http://127.0.0.1:' + (process.env.PORT || 3001) + '/api/health' },
{ name: 'auth-login', url: 'http://127.0.0.1:' + (process.env.PORT || 3001) + '/api/auth/login' },
]
async function probeOne(target) {
const t0 = Date.now()
try {
const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), 8000)
const r = await fetch(target.url, { method: 'GET', signal: ctrl.signal })
const latency = Date.now() - t0
const status = (r.ok || r.status === 400 || r.status === 401) ? 'up' : 'down'
const msg = 'HTTP ' + r.status
return { name: target.name, url: target.url, status, latencyMs: latency, message: msg }
} catch (e) {
return { name: target.name, url: target.url, status: 'down', latencyMs: Date.now() - t0, message: e.message || String(e) }
}
}
async function checkDb() {
const t0 = Date.now()
try {
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
await conn.execute('SELECT 1')
await conn.end()
return { name: 'database', url: 'mysql://' + (new URL(process.env.DATABASE_URL)).host, status: 'up', latencyMs: Date.now() - t0, message: 'ok' }
} catch (e) {
return { name: 'database', url: 'mysql', status: 'down', latencyMs: Date.now() - t0, message: e.message }
}
}
async function runOnce() {
const results = await Promise.all([...TARGETS.map(probeOne), checkDb()])
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
try {
for (const r of results) {
await conn.execute(
'INSERT INTO `ServiceCheck` (`name`,`url`,`status`,`latencyMs`,`message`,`lastCheckAt`,`createdAt`,`updatedAt`) VALUES (?,?,?,?,?,NOW(3),NOW(3),NOW(3)) ' +
'ON DUPLICATE KEY UPDATE `url`=VALUES(`url`),`status`=VALUES(`status`),`latencyMs`=VALUES(`latencyMs`),`message`=VALUES(`message`),`lastCheckAt`=NOW(3),`updatedAt`=NOW(3)',
[r.name, r.url, r.status, r.latencyMs, r.message.slice(0, 180)],
)
}
} finally {
await conn.end()
}
return results
}
module.exports = { runOnce, TARGETS }

120
server/komari-monitor.cjs Normal file
View File

@ -0,0 +1,120 @@
// komari 服务器监控拉取: 公开 API (无需 key)
// GET /api/nodes 节点列表
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
// GET /api/records/load?uuid=... 1 分钟粒度历史 (保留 24h)
const mysql = require('mysql2/promise')
const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
const TIMEOUT = 20000
function parseUrl(url) {
const u = new URL(url)
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
}
async function jget(path) {
const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), TIMEOUT)
const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal })
if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status)
const j = await r.json()
if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || ''))
return j.data
}
// 拉一次所有节点 + 最近状态 + 1 分钟历史, 入库
async function runOnce() {
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
const stats = { nodes: 0, snapshots: 0, history: 0, failed: 0 }
try {
const nodes = await jget('/api/nodes')
if (!Array.isArray(nodes)) throw new Error('nodes 格式异常')
stats.nodes = nodes.length
for (const n of nodes) {
const uuid = n.uuid
if (!uuid) continue
try {
const recent = await jget('/api/recent/' + uuid)
let snap = null
if (Array.isArray(recent) && recent.length) snap = recent[recent.length - 1]
const recordedAt = snap ? new Date(snap.updated_at) : new Date()
const ramTotal = snap && snap.ram ? Number(snap.ram.total) : Number(n.mem_total || 0)
const diskTotal = snap && snap.disk ? Number(snap.disk.total) : Number(n.disk_total || 0)
await conn.execute(
'INSERT INTO `ServerNode` (`uuid`,`name`,`cpuName`,`os`,`region`,`memTotal`,`diskTotal`,`cpuCores`,' +
'`cpuUsage`,`ramUsed`,`swapUsed`,`load1`,`load5`,`load15`,`diskUsed`,`netIn`,`netOut`,`uptime`,`process`,`connections`,`status`,`recordedAt`,`createdAt`,`updatedAt`) ' +
'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(3),NOW(3)) ' +
'ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`cpuName`=VALUES(`cpuName`),`os`=VALUES(`os`),`region`=VALUES(`region`),' +
'`memTotal`=VALUES(`memTotal`),`diskTotal`=VALUES(`diskTotal`),`cpuCores`=VALUES(`cpuCores`),' +
'`cpuUsage`=VALUES(`cpuUsage`),`ramUsed`=VALUES(`ramUsed`),`swapUsed`=VALUES(`swapUsed`),' +
'`load1`=VALUES(`load1`),`load5`=VALUES(`load5`),`load15`=VALUES(`load15`),`diskUsed`=VALUES(`diskUsed`),' +
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`uptime`=VALUES(`uptime`),' +
'`process`=VALUES(`process`),`connections`=VALUES(`connections`),`status`=VALUES(`status`),' +
'`recordedAt`=VALUES(`recordedAt`),`updatedAt`=NOW(3)',
[
uuid, n.name || '', n.cpu_name || '', n.os || '', n.region || '',
ramTotal, diskTotal, Number(n.cpu_cores || 0),
snap ? Number(snap.cpu?.usage || 0) : 0,
snap ? Number(snap.ram?.used || 0) : 0,
snap ? Number(snap.swap?.used || 0) : 0,
snap ? Number(snap.load?.load1 || 0) : 0,
snap ? Number(snap.load?.load5 || 0) : 0,
snap ? Number(snap.load?.load15 || 0) : 0,
snap ? Number(snap.disk?.used || 0) : 0,
snap ? Number(snap.network?.down || 0) : 0,
snap ? Number(snap.network?.up || 0) : 0,
snap ? Number(snap.uptime || 0) : 0,
snap ? Number(snap.process || 0) : 0,
snap ? Number(snap.connections?.tcp || 0) : 0,
snap ? 'online' : 'unknown',
recordedAt,
],
)
stats.snapshots++
let rec = null
try { rec = await jget('/api/records/load?uuid=' + uuid) } catch (e) { /* 节点未开启 record */ }
if (rec && Array.isArray(rec.records)) {
for (const r of rec.records) {
const period = new Date(r.time)
if (isNaN(period.getTime())) continue
const [res] = await conn.execute(
'INSERT INTO `ServerNodeHistory` (`uuid`,`period`,`cpu`,`ram`,`load`,`disk`,`netIn`,`netOut`,`process`,`connections`,`createdAt`) ' +
'VALUES (?,?,?,?,?,?,?,?,?,?,NOW(3)) ' +
'ON DUPLICATE KEY UPDATE `cpu`=VALUES(`cpu`),`ram`=VALUES(`ram`),`load`=VALUES(`load`),`disk`=VALUES(`disk`),' +
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`process`=VALUES(`process`),`connections`=VALUES(`connections`)',
[
uuid, period,
Number(r.cpu || 0), Number(r.ram || 0), Number(r.load || 0),
Number(r.disk || 0), Number(r.net_in || 0), Number(r.net_out || 0),
Number(r.process || 0), Number(r.connections || 0),
],
)
if (res.affectedRows === 1) stats.history++
}
}
} catch (e) {
stats.failed++
console.error('[komari] 节点 ' + uuid + ' 失败:', e.message)
}
}
} finally {
await conn.end()
}
return stats
}
// 清理 24h 之前的历史
async function cleanupOld() {
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
try {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000)
const [r] = await conn.execute('DELETE FROM `ServerNodeHistory` WHERE `period` < ?', [cutoff])
return r.affectedRows
} finally {
await conn.end()
}
}
module.exports = { runOnce, cleanupOld, BASE }

View File

@ -8,16 +8,21 @@ router.use(authRequired)
// 报警列表: 按时间最新优先, cursor 分页 (cursor = id) // 报警列表: 按时间最新优先, cursor 分页 (cursor = id)
router.get('/alerts', async (req, res, next) => { router.get('/alerts', async (req, res, next) => {
try { try {
const limit = Math.min(Number(req.query.limit) || 10, 50) const rawLimit = Number(req.query.limit)
const cursor = req.query.cursor ? Number(req.query.cursor) : null const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(Math.floor(rawLimit), 50) : 10
const rawCursor = Number(req.query.cursor)
const cursor = Number.isInteger(rawCursor) && rawCursor > 0 ? rawCursor : null
const where = cursor ? { id: { lt: cursor } } : {} const where = cursor ? { id: { lt: cursor } } : {}
const items = await prisma.alert.findMany({ const [items, total] = await Promise.all([
where, prisma.alert.findMany({
orderBy: { id: 'desc' }, where,
take: limit, orderBy: { id: 'desc' },
}) take: limit,
}),
prisma.alert.count(),
])
const nextCursor = items.length === limit ? String(items[items.length - 1].id) : null const nextCursor = items.length === limit ? String(items[items.length - 1].id) : null
res.json({ items, nextCursor }) res.json({ items, nextCursor, total })
} catch (e) { next(e) } } catch (e) { next(e) }
}) })

42
server/routes/monitors.js Normal file
View File

@ -0,0 +1,42 @@
import { Router } from 'express'
import { prisma } from '../db.js'
import { authRequired } from '../middleware.js'
const router = Router()
router.use(authRequired)
// 服务器节点列表: 元数据 + 最新快照
router.get('/servers', async (_req, res, next) => {
try {
const rows = await prisma.serverNode.findMany({
orderBy: { name: 'asc' },
})
res.json({ items: rows })
} catch (e) { next(e) }
})
// 节点时序: ?uuid=...&hours=24
router.get('/servers/:uuid/history', async (req, res, next) => {
try {
const hours = Math.min(Math.max(Number(req.query.hours) || 24, 1), 168)
const since = new Date(Date.now() - hours * 3600 * 1000)
const rows = await prisma.serverNodeHistory.findMany({
where: { uuid: req.params.uuid, period: { gte: since } },
orderBy: { period: 'asc' },
take: 2000,
})
res.json({ items: rows })
} catch (e) { next(e) }
})
// 服务健康列表
router.get('/services', async (_req, res, next) => {
try {
const rows = await prisma.serviceCheck.findMany({
orderBy: { name: 'asc' },
})
res.json({ items: rows })
} catch (e) { next(e) }
})
export default router

View File

@ -5,21 +5,47 @@ import { authRequired } from '../middleware.js'
const router = Router() const router = Router()
router.use(authRequired) router.use(authRequired)
// 新闻列表: 按时间最新优先, cursor 分页 (cursor = publishedAt ISO 字符串) // 新闻列表: 按时间最新优先, cursor 分页
// 用 prisma cursor 配合 id 兜底 (同一秒发布的新闻) // cursor 格式: "<publishedAt ISO>|<lastId>", 兼容旧版纯时间戳 (不带 |)
// 同秒多条时用 id 兜底, 避免翻页时整批漏掉
router.get('/news', async (req, res, next) => { router.get('/news', async (req, res, next) => {
try { try {
const limit = Math.min(Number(req.query.limit) || 10, 50) const rawLimit = Number(req.query.limit)
const cursor = req.query.cursor || null const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(Math.floor(rawLimit), 50) : 10
const where = cursor ? { publishedAt: { lt: new Date(cursor) } } : {} let where = {}
const cursor = req.query.cursor || ''
if (cursor) {
const [ts, idRaw] = cursor.split('|')
const c = new Date(ts)
if (!isNaN(c.getTime())) {
const id = Number(idRaw)
where = id > 0
? { OR: [{ publishedAt: { lt: c } }, { publishedAt: c, id: { lt: id } }] }
: { publishedAt: { lt: c } }
}
}
const items = await prisma.news.findMany({ const items = await prisma.news.findMany({
select: { id: true, title: true, summary: true, source: true, url: true, publishedAt: true, impactRank: true },
where, where,
orderBy: [{ publishedAt: 'desc' }, { id: 'desc' }], orderBy: [{ publishedAt: 'desc' }, { id: 'desc' }],
take: limit, take: limit,
}) })
const nextCursor = items.length === limit ? items[items.length - 1].publishedAt.toISOString() : null const nextCursor = items.length === limit && items.length > 0
? items[items.length - 1].publishedAt.toISOString() + '|' + items[items.length - 1].id
: null
res.json({ items, nextCursor }) res.json({ items, nextCursor })
} catch (e) { next(e) } } catch (e) { next(e) }
}) })
// 新闻详情: 含正文 content
router.get('/news/:id', async (req, res, next) => {
try {
const id = Number(req.params.id)
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: '无效的新闻 id' })
const item = await prisma.news.findUnique({ where: { id } })
if (!item) return res.status(404).json({ error: '新闻不存在' })
res.json({ item })
} catch (e) { next(e) }
})
export default router export default router

View File

@ -2,6 +2,7 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
import MobileShell from './layouts/MobileShell.jsx' import MobileShell from './layouts/MobileShell.jsx'
import LoginPage from './pages/LoginPage.jsx' import LoginPage from './pages/LoginPage.jsx'
import HomePage from './pages/HomePage.jsx' import HomePage from './pages/HomePage.jsx'
import NewsDetail from './pages/news/NewsDetail.jsx'
import MacroPage from './pages/MacroPage.jsx' import MacroPage from './pages/MacroPage.jsx'
import MonitorPage from './pages/MonitorPage.jsx' import MonitorPage from './pages/MonitorPage.jsx'
import MePage from './pages/MePage.jsx' import MePage from './pages/MePage.jsx'
@ -30,6 +31,7 @@ export default function App() {
<Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} /> <Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} />
<Route element={<RequireAuth><MobileShell /></RequireAuth>}> <Route element={<RequireAuth><MobileShell /></RequireAuth>}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/news/:id" element={<NewsDetail />} />
<Route path="/macro/*" element={<MacroPage />} /> <Route path="/macro/*" element={<MacroPage />} />
<Route path="/monitor" element={<MonitorPage />} /> <Route path="/monitor" element={<MonitorPage />} />
<Route path="/me" element={<MePage />} /> <Route path="/me" element={<MePage />} />

View File

@ -1,6 +1,8 @@
import { useEffect, useState, useRef, useCallback } from 'react' import { useEffect, useState, useRef, useCallback } from 'react'
import { NavBar, List, Card, Empty, DotLoading } from 'antd-mobile' import { useNavigate } from 'react-router-dom'
import { NavBar, Card, Empty, DotLoading, Badge } from 'antd-mobile'
import { api } from '../api/client.js' import { api } from '../api/client.js'
import SwipeTabs from '../components/SwipeTabs.jsx'
// : + // : +
// - : World Bank MCP (, ) () // - : World Bank MCP (, ) ()
@ -22,41 +24,62 @@ function fmtDate(s) {
} }
export default function HomePage() { export default function HomePage() {
const [active, setActive] = useState('news') //
const [alertCount, setAlertCount] = useState(0)
const handleAlertCount = useCallback((n) => setAlertCount(n), [])
const tabs = [
{
key: 'alerts',
label: (
<span className="tab-label">
报警
{alertCount > 0 && <Badge content={alertCount} />}
</span>
),
render: () => <AlertSection onCount={handleAlertCount} />,
},
{ key: 'news', label: '新闻', render: () => <NewsSection /> },
]
return ( return (
<div className="page-with-navbar"> <div className="page-with-navbar">
<NavBar back={null}>首页</NavBar> <NavBar back={null}>首页</NavBar>
<div className="home-content"> <div className="home-content">
<AlertSection /> <SwipeTabs tabs={tabs} active={active} onChange={setActive} />
<NewsSection />
</div> </div>
</div> </div>
) )
} }
// //
function AlertSection() { function AlertSection({ onCount }) {
const [items, setItems] = useState([]) const [items, setItems] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false) const [done, setDone] = useState(false)
const cursorRef = useRef(null) const cursorRef = useRef(null)
const sentinelRef = useRef(null) const sentinelRef = useRef(null)
const inFlightRef = useRef(false)
const fetchPage = useCallback(async () => { const fetchPage = useCallback(async () => {
if (loading || done) return if (inFlightRef.current || done) return
inFlightRef.current = true
setLoading(true) setLoading(true)
try { try {
const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '') const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '')
const data = await api(url) const data = await api(url)
setItems((prev) => prev.concat(data.items || [])) setItems((prev) => prev.concat(data.items || []))
onCount?.(data.total ?? (data.items?.length || 0))
cursorRef.current = data.nextCursor cursorRef.current = data.nextCursor
if (!data.nextCursor) setDone(true) if (!data.nextCursor) setDone(true)
} catch (e) { } catch (e) {
console.error('alerts load error', e) console.error('alerts load error', e)
setDone(true) setDone(true)
} finally { } finally {
inFlightRef.current = false
setLoading(false) setLoading(false)
} }
}, [loading, done]) }, [done, onCount])
useEffect(() => { fetchPage() }, []) useEffect(() => { fetchPage() }, [])
useEffect(() => { useEffect(() => {
@ -106,9 +129,12 @@ function NewsSection() {
const [done, setDone] = useState(false) const [done, setDone] = useState(false)
const cursorRef = useRef(null) const cursorRef = useRef(null)
const sentinelRef = useRef(null) const sentinelRef = useRef(null)
const inFlightRef = useRef(false)
const navigate = useNavigate()
const fetchPage = useCallback(async () => { const fetchPage = useCallback(async () => {
if (loading || done) return if (inFlightRef.current || done) return
inFlightRef.current = true
setLoading(true) setLoading(true)
try { try {
const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '') const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '')
@ -120,9 +146,10 @@ function NewsSection() {
console.error('news load error', e) console.error('news load error', e)
setDone(true) setDone(true)
} finally { } finally {
inFlightRef.current = false
setLoading(false) setLoading(false)
} }
}, [loading, done]) }, [done])
useEffect(() => { fetchPage() }, []) useEffect(() => { fetchPage() }, [])
useEffect(() => { useEffect(() => {
@ -144,7 +171,7 @@ function NewsSection() {
) : ( ) : (
<div className="section-list"> <div className="section-list">
{items.map((n) => ( {items.map((n) => (
<Card key={n.id} className="news-card"> <Card key={n.id} className="news-card" onClick={() => navigate('/news/' + n.id)}>
<div className="news-title">{n.title}</div> <div className="news-title">{n.title}</div>
{n.summary && <div className="news-summary">{n.summary}</div>} {n.summary && <div className="news-summary">{n.summary}</div>}
<div className="news-meta"> <div className="news-meta">

View File

@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { NavBar } from 'antd-mobile'
import { api } from '../../api/client.js'
// : /news/:id
// News.content (MCP ), content,
function fmtDate(s) {
if (!s) return ''
const d = new Date(s)
if (isNaN(d.getTime())) return s
const pad = (n) => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
}
export default function NewsDetail() {
const { id } = useParams()
const navigate = useNavigate()
const [item, setItem] = useState(null)
const [err, setErr] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
setLoading(true)
api('/news/' + encodeURIComponent(id))
.then((data) => { if (!cancelled) setItem(data.item) })
.catch((e) => !cancelled && setErr(e.message))
.finally(() => !cancelled && setLoading(false))
return () => { cancelled = true }
}, [id])
return (
<div className="article-page">
<NavBar onBack={() => navigate(-1)}>新闻详情</NavBar>
{loading && <div className="loading">加载中...</div>}
{err && <div className="error-banner">{err}</div>}
{!loading && !err && !item && <div className="empty">新闻不存在</div>}
{!loading && !err && item && (
<div className="news-detail">
<h1>{item.title}</h1>
<div className="meta">
{item.source || '未知来源'} · {fmtDate(item.publishedAt)} · #{item.impactRank}
{/^https?:\/\//i.test(item.url || '') && (
<a className="news-origin" href={item.url} target="_blank" rel="noreferrer">原文链接</a>
)}
</div>
<div className="news-content">
{item.content ? item.content : item.summary}
{!item.content && <div className="news-content-empty">本条暂无全文仅摘要</div>}
</div>
</div>
)}
</div>
)
}

View File

@ -197,6 +197,8 @@ input, textarea, select { font: inherit; }
.me-logout { color: var(--color-danger); } .me-logout { color: var(--color-danger); }
/* 首页模块: 报警 + 新闻 */ /* 首页模块: 报警 + 新闻 */
.home-content { padding: 0 0 16px; } .home-content { padding: 0 0 16px; }
.home-content .adm-tabs-content { --content-padding: 0; }
.home-content .tab-label { display: inline-flex; align-items: center; gap: 4px; }
.home-section { padding: 0 12px 16px; } .home-section { padding: 0 12px 16px; }
.section-title { font-size: 16px; font-weight: 700; padding: 14px 4px 8px; color: var(--color-text); } .section-title { font-size: 16px; font-weight: 700; padding: 14px 4px 8px; color: var(--color-text); }
.section-list { display: flex; flex-direction: column; gap: 8px; } .section-list { display: flex; flex-direction: column; gap: 8px; }
@ -214,4 +216,12 @@ input, textarea, select { font: inherit; }
.news-summary { font-size: 12px; color: var(--color-muted); margin-top: 4px; line-height: 1.5; max-height: 3.6em; overflow: hidden; } .news-summary { font-size: 12px; color: var(--color-muted); margin-top: 4px; line-height: 1.5; max-height: 3.6em; overflow: hidden; }
.news-meta { display: flex; gap: 10px; font-size: 11px; color: var(--color-muted); margin-top: 6px; } .news-meta { display: flex; gap: 10px; font-size: 11px; color: var(--color-muted); margin-top: 6px; }
.news-rank { color: var(--color-primary); font-weight: 600; } .news-rank { color: var(--color-primary); font-weight: 600; }
.news-card:active .adm-card-body { background: #f5f5f5; }
/* 新闻详情页 */
.news-detail { background: #fff; padding: 18px 16px 24px; }
.news-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
.news-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.news-origin { color: var(--color-primary); margin-left: auto; }
.news-content { font-size: 15px; line-height: 1.75; color: #333; white-space: pre-wrap; word-break: break-word; }
.news-content-empty { margin-top: 12px; font-size: 12px; color: var(--color-muted); }
.scroll-sentinel { text-align: center; padding: 14px 0 4px; color: var(--color-muted); font-size: 12px; } .scroll-sentinel { text-align: center; padding: 14px 0 4px; color: var(--color-muted); font-size: 12px; }