更新首页
This commit is contained in:
parent
547181d333
commit
4d02439ecb
5
.gitignore
vendored
5
.gitignore
vendored
@ -31,6 +31,7 @@ lerna-debug.log*
|
||||
*.swo
|
||||
*~
|
||||
dist/*
|
||||
|
||||
# local env (contains API keys)
|
||||
|
||||
# local env (contains API keys)
|
||||
/.env
|
||||
.reasonix/*
|
||||
@ -52,6 +52,21 @@ export type News = Prisma.NewsModel
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
||||
@ -76,6 +76,21 @@ export type News = Prisma.NewsModel
|
||||
*
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
||||
@ -205,6 +205,56 @@ export type StringNullableFilter<$PrismaModel = never> = {
|
||||
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> =
|
||||
| Prisma.PatchUndefined<
|
||||
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
|
||||
}
|
||||
|
||||
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> =
|
||||
| Prisma.PatchUndefined<
|
||||
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
|
||||
}
|
||||
|
||||
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> =
|
||||
| Prisma.PatchUndefined<
|
||||
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -391,6 +391,9 @@ export const ModelName = {
|
||||
IndicatorPoint: 'IndicatorPoint',
|
||||
News: 'News',
|
||||
Alert: 'Alert',
|
||||
ServerNode: 'ServerNode',
|
||||
ServerNodeHistory: 'ServerNodeHistory',
|
||||
ServiceCheck: 'ServiceCheck',
|
||||
Econ_SowInventory: 'Econ_SowInventory',
|
||||
ScraperCache: 'ScraperCache'
|
||||
} as const
|
||||
@ -408,7 +411,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
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
|
||||
}
|
||||
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: {
|
||||
payload: Prisma.$Econ_SowInventoryPayload<ExtArgs>
|
||||
fields: Prisma.Econ_SowInventoryFieldRefs
|
||||
@ -1114,6 +1315,7 @@ export const NewsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
summary: 'summary',
|
||||
content: 'content',
|
||||
source: 'source',
|
||||
url: 'url',
|
||||
publishedAt: 'publishedAt',
|
||||
@ -1140,6 +1342,70 @@ export const 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 = {
|
||||
id: 'id',
|
||||
month: 'month',
|
||||
@ -1250,9 +1516,18 @@ export const 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 = {
|
||||
title: 'title',
|
||||
summary: 'summary',
|
||||
content: 'content',
|
||||
source: 'source',
|
||||
url: 'url'
|
||||
} as const
|
||||
@ -1270,6 +1545,35 @@ export const 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 = {
|
||||
month: 'month'
|
||||
} as const
|
||||
@ -1277,14 +1581,6 @@ export const 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 = {
|
||||
scraperName: 'scraperName',
|
||||
lastDataHash: 'lastDataHash'
|
||||
@ -1347,6 +1643,13 @@ export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel,
|
||||
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
|
||||
*/
|
||||
@ -1449,6 +1752,9 @@ export type GlobalOmitConfig = {
|
||||
indicatorPoint?: Prisma.IndicatorPointOmit
|
||||
news?: Prisma.NewsOmit
|
||||
alert?: Prisma.AlertOmit
|
||||
serverNode?: Prisma.ServerNodeOmit
|
||||
serverNodeHistory?: Prisma.ServerNodeHistoryOmit
|
||||
serviceCheck?: Prisma.ServiceCheckOmit
|
||||
econ_SowInventory?: Prisma.Econ_SowInventoryOmit
|
||||
scraperCache?: Prisma.ScraperCacheOmit
|
||||
}
|
||||
|
||||
@ -58,6 +58,9 @@ export const ModelName = {
|
||||
IndicatorPoint: 'IndicatorPoint',
|
||||
News: 'News',
|
||||
Alert: 'Alert',
|
||||
ServerNode: 'ServerNode',
|
||||
ServerNodeHistory: 'ServerNodeHistory',
|
||||
ServiceCheck: 'ServiceCheck',
|
||||
Econ_SowInventory: 'Econ_SowInventory',
|
||||
ScraperCache: 'ScraperCache'
|
||||
} as const
|
||||
@ -147,6 +150,7 @@ export const NewsScalarFieldEnum = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
summary: 'summary',
|
||||
content: 'content',
|
||||
source: 'source',
|
||||
url: 'url',
|
||||
publishedAt: 'publishedAt',
|
||||
@ -173,6 +177,70 @@ export const 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 = {
|
||||
id: 'id',
|
||||
month: 'month',
|
||||
@ -283,9 +351,18 @@ export const 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 = {
|
||||
title: 'title',
|
||||
summary: 'summary',
|
||||
content: 'content',
|
||||
source: 'source',
|
||||
url: 'url'
|
||||
} as const
|
||||
@ -303,6 +380,35 @@ export const 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 = {
|
||||
month: 'month'
|
||||
} as const
|
||||
@ -310,14 +416,6 @@ export const 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 = {
|
||||
scraperName: 'scraperName',
|
||||
lastDataHash: 'lastDataHash'
|
||||
|
||||
@ -15,6 +15,9 @@ export type * from './models/Indicator.ts'
|
||||
export type * from './models/IndicatorPoint.ts'
|
||||
export type * from './models/News.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/ScraperCache.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
@ -40,6 +40,7 @@ export type NewsMinAggregateOutputType = {
|
||||
id: number | null
|
||||
title: string | null
|
||||
summary: string | null
|
||||
content: string | null
|
||||
source: string | null
|
||||
url: string | null
|
||||
publishedAt: Date | null
|
||||
@ -51,6 +52,7 @@ export type NewsMaxAggregateOutputType = {
|
||||
id: number | null
|
||||
title: string | null
|
||||
summary: string | null
|
||||
content: string | null
|
||||
source: string | null
|
||||
url: string | null
|
||||
publishedAt: Date | null
|
||||
@ -62,6 +64,7 @@ export type NewsCountAggregateOutputType = {
|
||||
id: number
|
||||
title: number
|
||||
summary: number
|
||||
content: number
|
||||
source: number
|
||||
url: number
|
||||
publishedAt: number
|
||||
@ -85,6 +88,7 @@ export type NewsMinAggregateInputType = {
|
||||
id?: true
|
||||
title?: true
|
||||
summary?: true
|
||||
content?: true
|
||||
source?: true
|
||||
url?: true
|
||||
publishedAt?: true
|
||||
@ -96,6 +100,7 @@ export type NewsMaxAggregateInputType = {
|
||||
id?: true
|
||||
title?: true
|
||||
summary?: true
|
||||
content?: true
|
||||
source?: true
|
||||
url?: true
|
||||
publishedAt?: true
|
||||
@ -107,6 +112,7 @@ export type NewsCountAggregateInputType = {
|
||||
id?: true
|
||||
title?: true
|
||||
summary?: true
|
||||
content?: true
|
||||
source?: true
|
||||
url?: true
|
||||
publishedAt?: true
|
||||
@ -205,6 +211,7 @@ export type NewsGroupByOutputType = {
|
||||
id: number
|
||||
title: string
|
||||
summary: string
|
||||
content: string | null
|
||||
source: string
|
||||
url: string
|
||||
publishedAt: Date
|
||||
@ -239,6 +246,7 @@ export type NewsWhereInput = {
|
||||
id?: Prisma.IntFilter<"News"> | number
|
||||
title?: Prisma.StringFilter<"News"> | string
|
||||
summary?: Prisma.StringFilter<"News"> | string
|
||||
content?: Prisma.StringNullableFilter<"News"> | string | null
|
||||
source?: Prisma.StringFilter<"News"> | string
|
||||
url?: Prisma.StringFilter<"News"> | string
|
||||
publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string
|
||||
@ -250,6 +258,7 @@ export type NewsOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
summary?: Prisma.SortOrder
|
||||
content?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
source?: Prisma.SortOrder
|
||||
url?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
@ -266,6 +275,7 @@ export type NewsWhereUniqueInput = Prisma.AtLeast<{
|
||||
NOT?: Prisma.NewsWhereInput | Prisma.NewsWhereInput[]
|
||||
title?: Prisma.StringFilter<"News"> | string
|
||||
summary?: Prisma.StringFilter<"News"> | string
|
||||
content?: Prisma.StringNullableFilter<"News"> | string | null
|
||||
source?: Prisma.StringFilter<"News"> | string
|
||||
url?: Prisma.StringFilter<"News"> | string
|
||||
publishedAt?: Prisma.DateTimeFilter<"News"> | Date | string
|
||||
@ -277,6 +287,7 @@ export type NewsOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
summary?: Prisma.SortOrder
|
||||
content?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
source?: Prisma.SortOrder
|
||||
url?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
@ -296,6 +307,7 @@ export type NewsScalarWhereWithAggregatesInput = {
|
||||
id?: Prisma.IntWithAggregatesFilter<"News"> | number
|
||||
title?: Prisma.StringWithAggregatesFilter<"News"> | string
|
||||
summary?: Prisma.StringWithAggregatesFilter<"News"> | string
|
||||
content?: Prisma.StringNullableWithAggregatesFilter<"News"> | string | null
|
||||
source?: Prisma.StringWithAggregatesFilter<"News"> | string
|
||||
url?: Prisma.StringWithAggregatesFilter<"News"> | string
|
||||
publishedAt?: Prisma.DateTimeWithAggregatesFilter<"News"> | Date | string
|
||||
@ -306,6 +318,7 @@ export type NewsScalarWhereWithAggregatesInput = {
|
||||
export type NewsCreateInput = {
|
||||
title: string
|
||||
summary?: string
|
||||
content?: string | null
|
||||
source?: string
|
||||
url?: string
|
||||
publishedAt: Date | string
|
||||
@ -317,6 +330,7 @@ export type NewsUncheckedCreateInput = {
|
||||
id?: number
|
||||
title: string
|
||||
summary?: string
|
||||
content?: string | null
|
||||
source?: string
|
||||
url?: string
|
||||
publishedAt: Date | string
|
||||
@ -327,6 +341,7 @@ export type NewsUncheckedCreateInput = {
|
||||
export type NewsUpdateInput = {
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
summary?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
url?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -338,6 +353,7 @@ export type NewsUncheckedUpdateInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
summary?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
url?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -349,6 +365,7 @@ export type NewsCreateManyInput = {
|
||||
id?: number
|
||||
title: string
|
||||
summary?: string
|
||||
content?: string | null
|
||||
source?: string
|
||||
url?: string
|
||||
publishedAt: Date | string
|
||||
@ -359,6 +376,7 @@ export type NewsCreateManyInput = {
|
||||
export type NewsUpdateManyMutationInput = {
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
summary?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
url?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -370,6 +388,7 @@ export type NewsUncheckedUpdateManyInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
summary?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
content?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
url?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
publishedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
@ -392,6 +411,7 @@ export type NewsCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
summary?: Prisma.SortOrder
|
||||
content?: Prisma.SortOrder
|
||||
source?: Prisma.SortOrder
|
||||
url?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
@ -408,6 +428,7 @@ export type NewsMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
summary?: Prisma.SortOrder
|
||||
content?: Prisma.SortOrder
|
||||
source?: Prisma.SortOrder
|
||||
url?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
@ -419,6 +440,7 @@ export type NewsMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
title?: Prisma.SortOrder
|
||||
summary?: Prisma.SortOrder
|
||||
content?: Prisma.SortOrder
|
||||
source?: Prisma.SortOrder
|
||||
url?: Prisma.SortOrder
|
||||
publishedAt?: Prisma.SortOrder
|
||||
@ -431,12 +453,17 @@ export type NewsSumOrderByAggregateInput = {
|
||||
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<{
|
||||
id?: boolean
|
||||
title?: boolean
|
||||
summary?: boolean
|
||||
content?: boolean
|
||||
source?: boolean
|
||||
url?: boolean
|
||||
publishedAt?: boolean
|
||||
@ -450,6 +477,7 @@ export type NewsSelectScalar = {
|
||||
id?: boolean
|
||||
title?: boolean
|
||||
summary?: boolean
|
||||
content?: boolean
|
||||
source?: boolean
|
||||
url?: boolean
|
||||
publishedAt?: boolean
|
||||
@ -457,7 +485,7 @@ export type NewsSelectScalar = {
|
||||
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> = {
|
||||
name: "News"
|
||||
@ -466,6 +494,7 @@ export type $NewsPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
id: number
|
||||
title: string
|
||||
summary: string
|
||||
content: string | null
|
||||
source: string
|
||||
url: string
|
||||
publishedAt: Date
|
||||
@ -843,6 +872,7 @@ export interface NewsFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"News", 'Int'>
|
||||
readonly title: Prisma.FieldRef<"News", 'String'>
|
||||
readonly summary: Prisma.FieldRef<"News", 'String'>
|
||||
readonly content: Prisma.FieldRef<"News", 'String'>
|
||||
readonly source: Prisma.FieldRef<"News", 'String'>
|
||||
readonly url: Prisma.FieldRef<"News", 'String'>
|
||||
readonly publishedAt: Prisma.FieldRef<"News", 'DateTime'>
|
||||
|
||||
@ -391,10 +391,6 @@ export type ScraperCacheSumOrderByAggregateInput = {
|
||||
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<{
|
||||
|
||||
1702
generated/prisma/models/ServerNode.ts
Normal file
1702
generated/prisma/models/ServerNode.ts
Normal file
File diff suppressed because it is too large
Load Diff
1320
generated/prisma/models/ServerNodeHistory.ts
Normal file
1320
generated/prisma/models/ServerNodeHistory.ts
Normal file
File diff suppressed because it is too large
Load Diff
1194
generated/prisma/models/ServiceCheck.ts
Normal file
1194
generated/prisma/models/ServiceCheck.ts
Normal file
File diff suppressed because it is too large
Load Diff
24
package-lock.json
generated
24
package-lock.json
generated
@ -24,7 +24,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.8.1",
|
||||
"ws": "^8.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
@ -6360,6 +6361,27 @@
|
||||
"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": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.8.1",
|
||||
"ws": "^8.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
|
||||
68
prisma/migrations/20260806140000_monitors/migration.sql
Normal file
68
prisma/migrations/20260806140000_monitors/migration.sql
Normal 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;
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterTable (news.content 全文, 须在 news_and_alerts 建表之后执行)
|
||||
ALTER TABLE `News` ADD COLUMN `content` TEXT NULL;
|
||||
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
||||
|
||||
@ -72,6 +72,7 @@ model News {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
summary String @default("")
|
||||
content String? @db.Text
|
||||
source String @default("")
|
||||
url String @default("")
|
||||
publishedAt DateTime
|
||||
@ -98,6 +99,69 @@ model Alert {
|
||||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
month String @unique
|
||||
|
||||
72
scripts/.fix-cron.cjs
Normal file
72
scripts/.fix-cron.cjs
Normal 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)
|
||||
@ -50,15 +50,17 @@ function parseNewsSheet(sheet, impactBase = 0) {
|
||||
const out = []
|
||||
for (let i = 0; i < sheet.items.length; i++) {
|
||||
const r = sheet.items[i]
|
||||
const title = String(r[0] || '').trim()
|
||||
const title = String(r[0] || '').trim().slice(0, 190)
|
||||
if (!title) continue
|
||||
const publishedAt = parsePublishedAt(r[2])
|
||||
if (!publishedAt) continue
|
||||
const body = String(r[1] || '').trim()
|
||||
out.push({
|
||||
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),
|
||||
url: String(r[4] || '').trim().slice(0, 400),
|
||||
url: String(r[4] || '').trim().slice(0, 190), // 列宽 varchar(191), 超长会被 MySQL 拒绝
|
||||
publishedAt,
|
||||
impactRank: impactBase + i + 1,
|
||||
})
|
||||
@ -103,20 +105,27 @@ export async function persistNews(newsList) {
|
||||
if (!process.env.DATABASE_URL) throw new Error('缺少 DATABASE_URL')
|
||||
if (!newsList || newsList.length === 0) return { inserted: 0, skipped: 0 }
|
||||
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||
let inserted = 0, skipped = 0
|
||||
let inserted = 0, skipped = 0, failed = 0
|
||||
try {
|
||||
for (const n of newsList) {
|
||||
const [r] = await conn.execute(
|
||||
'INSERT INTO `News` (`title`, `summary`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, NOW(3)) ' +
|
||||
'ON DUPLICATE KEY UPDATE `impactRank` = VALUES(`impactRank`)',
|
||||
[n.title, n.summary, n.source, n.url, n.publishedAt, n.impactRank],
|
||||
)
|
||||
if (r.affectedRows === 1) inserted++
|
||||
else skipped++
|
||||
try {
|
||||
// 注意: content 列依赖迁移 add_news_content 已应用, 发布顺序须先迁移后跑本脚本
|
||||
// 重复命中 (title, publishedAt) 时只回填仍为 NULL 的 content, 避免覆盖已有全文
|
||||
const [r] = await conn.execute(
|
||||
'INSERT INTO `News` (`title`, `summary`, `content`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3)) ' +
|
||||
'AS new ON DUPLICATE KEY UPDATE `impactRank` = new.`impactRank`, `content` = IFNULL(News.`content`, new.`content`)',
|
||||
[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 {
|
||||
await conn.end()
|
||||
}
|
||||
return { inserted, skipped }
|
||||
return { inserted, skipped, failed }
|
||||
}
|
||||
@ -7,6 +7,6 @@ async function main() {
|
||||
console.log('[News] 拉取 ' + list.length + ' 条, 耗时 ' + ((Date.now() - t0) / 1000).toFixed(1) + 's')
|
||||
if (list.length === 0) { console.log('[News] 无数据, 跳过'); return }
|
||||
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) })
|
||||
|
||||
@ -33,4 +33,36 @@ function scheduleNews() {
|
||||
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()
|
||||
|
||||
55
server/health-check.cjs
Normal file
55
server/health-check.cjs
Normal 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
120
server/komari-monitor.cjs
Normal 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 }
|
||||
|
||||
@ -8,16 +8,21 @@ router.use(authRequired)
|
||||
// 报警列表: 按时间最新优先, cursor 分页 (cursor = id)
|
||||
router.get('/alerts', async (req, res, next) => {
|
||||
try {
|
||||
const limit = Math.min(Number(req.query.limit) || 10, 50)
|
||||
const cursor = req.query.cursor ? Number(req.query.cursor) : null
|
||||
const rawLimit = Number(req.query.limit)
|
||||
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 items = await prisma.alert.findMany({
|
||||
where,
|
||||
orderBy: { id: 'desc' },
|
||||
take: limit,
|
||||
})
|
||||
const [items, total] = await Promise.all([
|
||||
prisma.alert.findMany({
|
||||
where,
|
||||
orderBy: { id: 'desc' },
|
||||
take: limit,
|
||||
}),
|
||||
prisma.alert.count(),
|
||||
])
|
||||
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) }
|
||||
})
|
||||
|
||||
|
||||
42
server/routes/monitors.js
Normal file
42
server/routes/monitors.js
Normal 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
|
||||
@ -5,21 +5,47 @@ import { authRequired } from '../middleware.js'
|
||||
const router = Router()
|
||||
router.use(authRequired)
|
||||
|
||||
// 新闻列表: 按时间最新优先, cursor 分页 (cursor = publishedAt ISO 字符串)
|
||||
// 用 prisma cursor 配合 id 兜底 (同一秒发布的新闻)
|
||||
// 新闻列表: 按时间最新优先, cursor 分页
|
||||
// cursor 格式: "<publishedAt ISO>|<lastId>", 兼容旧版纯时间戳 (不带 |)
|
||||
// 同秒多条时用 id 兜底, 避免翻页时整批漏掉
|
||||
router.get('/news', async (req, res, next) => {
|
||||
try {
|
||||
const limit = Math.min(Number(req.query.limit) || 10, 50)
|
||||
const cursor = req.query.cursor || null
|
||||
const where = cursor ? { publishedAt: { lt: new Date(cursor) } } : {}
|
||||
const rawLimit = Number(req.query.limit)
|
||||
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(Math.floor(rawLimit), 50) : 10
|
||||
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({
|
||||
select: { id: true, title: true, summary: true, source: true, url: true, publishedAt: true, impactRank: true },
|
||||
where,
|
||||
orderBy: [{ publishedAt: 'desc' }, { id: 'desc' }],
|
||||
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 })
|
||||
} 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
|
||||
|
||||
@ -2,6 +2,7 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
|
||||
import MobileShell from './layouts/MobileShell.jsx'
|
||||
import LoginPage from './pages/LoginPage.jsx'
|
||||
import HomePage from './pages/HomePage.jsx'
|
||||
import NewsDetail from './pages/news/NewsDetail.jsx'
|
||||
import MacroPage from './pages/MacroPage.jsx'
|
||||
import MonitorPage from './pages/MonitorPage.jsx'
|
||||
import MePage from './pages/MePage.jsx'
|
||||
@ -30,6 +31,7 @@ export default function App() {
|
||||
<Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} />
|
||||
<Route element={<RequireAuth><MobileShell /></RequireAuth>}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/news/:id" element={<NewsDetail />} />
|
||||
<Route path="/macro/*" element={<MacroPage />} />
|
||||
<Route path="/monitor" element={<MonitorPage />} />
|
||||
<Route path="/me" element={<MePage />} />
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
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 SwipeTabs from '../components/SwipeTabs.jsx'
|
||||
|
||||
// 首页: 报警模块 + 新闻模块
|
||||
// - 报警: World Bank 与妙想 MCP 跨源同 (指标, 周期) 数据差异超阈值 (按时间倒序)
|
||||
@ -22,41 +24,62 @@ function fmtDate(s) {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="page-with-navbar">
|
||||
<NavBar back={null}>首页</NavBar>
|
||||
<div className="home-content">
|
||||
<AlertSection />
|
||||
<NewsSection />
|
||||
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 报警模块
|
||||
function AlertSection() {
|
||||
function AlertSection({ onCount }) {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
const cursorRef = useRef(null)
|
||||
const sentinelRef = useRef(null)
|
||||
const inFlightRef = useRef(false)
|
||||
|
||||
const fetchPage = useCallback(async () => {
|
||||
if (loading || done) return
|
||||
if (inFlightRef.current || done) return
|
||||
inFlightRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '')
|
||||
const data = await api(url)
|
||||
setItems((prev) => prev.concat(data.items || []))
|
||||
onCount?.(data.total ?? (data.items?.length || 0))
|
||||
cursorRef.current = data.nextCursor
|
||||
if (!data.nextCursor) setDone(true)
|
||||
} catch (e) {
|
||||
console.error('alerts load error', e)
|
||||
setDone(true)
|
||||
} finally {
|
||||
inFlightRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
}, [loading, done])
|
||||
}, [done, onCount])
|
||||
|
||||
useEffect(() => { fetchPage() }, [])
|
||||
useEffect(() => {
|
||||
@ -106,9 +129,12 @@ function NewsSection() {
|
||||
const [done, setDone] = useState(false)
|
||||
const cursorRef = useRef(null)
|
||||
const sentinelRef = useRef(null)
|
||||
const inFlightRef = useRef(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const fetchPage = useCallback(async () => {
|
||||
if (loading || done) return
|
||||
if (inFlightRef.current || done) return
|
||||
inFlightRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '')
|
||||
@ -120,9 +146,10 @@ function NewsSection() {
|
||||
console.error('news load error', e)
|
||||
setDone(true)
|
||||
} finally {
|
||||
inFlightRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
}, [loading, done])
|
||||
}, [done])
|
||||
|
||||
useEffect(() => { fetchPage() }, [])
|
||||
useEffect(() => {
|
||||
@ -144,7 +171,7 @@ function NewsSection() {
|
||||
) : (
|
||||
<div className="section-list">
|
||||
{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>
|
||||
{n.summary && <div className="news-summary">{n.summary}</div>}
|
||||
<div className="news-meta">
|
||||
|
||||
56
src/pages/news/NewsDetail.jsx
Normal file
56
src/pages/news/NewsDetail.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -197,6 +197,8 @@ input, textarea, select { font: inherit; }
|
||||
.me-logout { color: var(--color-danger); }
|
||||
/* 首页模块: 报警 + 新闻 */
|
||||
.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; }
|
||||
.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; }
|
||||
@ -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-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-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; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user