Files
zpan/shared/types/index.ts
T
Jasper Van c85e60f200 fix(analytics): enforce trustworthy offline statistics (#510)
* fix(analytics): enforce trustworthy offline statistics

Separate immutable counters from point-in-time snapshots, expose incomplete coverage instead of synthetic zeroes, and keep browser analytics result-only.

Restore finite Free quota baselines, fail closed for invalid storage quota, reconcile traffic reports fairly, and add production-safe backfill and data-quality diagnostics.

* fix(analytics): preserve global backfill totals

Group generated hourly backfill rows by their projected values so SQLite cannot resolve output aliases to source organization columns and overwrite cross-organization totals.
2026-07-20 10:23:22 -04:00

697 lines
16 KiB
TypeScript

import type { CommercePayment, CommerceProduct, ProductPrice } from 'zpan-cloud-sdk'
import type { DirType, ObjectStatus, StorageStatus } from '../constants'
import type {
CloudOrder as ZPanCloudOrder,
CloudOrderFulfillmentPayload as ZPanCloudOrderFulfillmentPayload,
CloudOrderItem as ZPanCloudOrderItem,
} from '../schemas/cloud-store'
export interface StorageObject {
id: string
orgId: string
alias: string
name: string
type: string
size: number
dirtype: DirType
parent: string
object: string
storageId: string
status: ObjectStatus
// Soft-delete marker: null = live, epoch ms = in trash.
trashedAt: number | null
createdAt: string
updatedAt: string
}
export interface Storage {
id: string
provider: string
bucket: string
endpoint: string
region: string
accessKey: string
secretKey: string
filePath: string
customHost: string | null
capacity: number
forcePathStyle: boolean
egressCreditBillingEnabled: boolean
egressCreditUnitBytes: number
egressCreditPerUnit: number
used: number
status: StorageStatus
createdAt: string
updatedAt: string
}
export interface OrgQuota {
id: string
orgId: string
baseQuota: number
entitlementQuota: number
quota: number
used: number
baseTrafficQuota: number
entitlementTrafficQuota: number
trafficQuota: number
trafficUsed: number
trafficPeriod: string
storagePlanName: string | null
storageExtraNames: string[]
trafficPlanName: string | null
trafficExtraNames: string[]
currentPlan?: CurrentStoragePlan | null
}
export interface OrgQuotaEntitlement {
id: string
orgId: string
resourceType: 'storage' | 'traffic' | string
entitlementType: 'plan' | 'campaign' | 'grant' | string
source: string
sourceId: string
bytes: number
startsAt: string
expiresAt: string | null
status: string
metadata: string | null
createdAt: string
updatedAt: string
}
export interface CurrentStoragePlan {
sourceId: string
packageId: string | null
name: string
storageBytes: number
trafficBytes: number
trafficOveragePriceCents: number | null
expiresAt: string | null
subscription: boolean
}
export type WebhookEventStatus = 'processed' | 'duplicate' | 'failed'
export type CloudProduct = CommerceProduct
export type CloudProductPrice = ProductPrice
export type CloudOrderFulfillmentPayload = ZPanCloudOrderFulfillmentPayload
export type CloudOrderItem = ZPanCloudOrderItem
export interface CloudOrderTarget {
orgId?: string
customerId?: string
customerLabel?: string
}
export type CloudOrderPayment = CommercePayment
export type CloudOrder = ZPanCloudOrder
export interface CloudGiftCard {
id: string
storeId: string
campaignId: string | null
code: string | null
codeLast4: string
credits: number
status: 'active' | 'redeemed' | 'disabled' | 'expired' | 'revoked'
expiresAt: string | null
createdAt: string
updatedAt: string
disabledAt: string | null
revokedAt: string | null
createdByAdmin: string
}
export interface CloudStoreTarget {
orgId: string
name: string
type: string
role: string
}
export interface Organization {
id: string
name: string
slug: string
logo: string | null
metadata: string | null
createdAt: string
updatedAt: string | null
}
export interface Member {
id: string
organizationId: string
userId: string
role: string
createdAt: string
}
export interface Invitation {
id: string
organizationId: string
email: string
role: string
status: 'pending' | 'accepted' | 'rejected' | 'canceled'
expiresAt: string
inviterId: string
createdAt: string
}
export type SiteInvitationStatus = 'pending' | 'accepted' | 'expired' | 'revoked'
export interface SiteInvitation {
id: string
email: string
token: string
invitedBy: string
invitedByName: string
acceptedBy: string | null
acceptedAt: string | null
revokedBy: string | null
revokedAt: string | null
expiresAt: string
createdAt: string
updatedAt: string
status: SiteInvitationStatus
}
export interface SystemOption {
key: string
value: string
public: boolean
}
// One monomorphic shape for every caller. Role changes values only: admin sees a
// masked `clientSecret`, front-of-house sees `clientSecret: null` (and the enabled-only list).
export interface AuthProvider {
providerId: string
type: string
enabled: boolean
name: string
icon: string
clientId: string
discoveryUrl: string | null
scopes: string[] | null
callbackUri: string
clientSecret: string | null
}
export interface AuthProviderList {
items: AuthProvider[]
callbackBaseUri: string
}
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
}
export type {
AdminDashboardGrowthStats,
AdminDashboardOperationsStats,
AdminDashboardOverviewStats,
AdminDashboardSharingStats,
AdminDashboardStorageStats,
AdminDashboardTrafficStats,
AdminSharingDataQuality,
AdminStatsCoverage,
AdminStatsDelta,
AdminStatsRange,
AdminStorageDataQuality,
AdminTopShare,
AdminTransferDataQuality,
AdminUsageBySpace,
} from './admin-stats'
export type DownloaderStatus = 'online' | 'offline' | 'disabled'
export type DownloaderEngine = 'http' | 'aria2' | 'qbittorrent'
// `Downloader` is inferred from `downloaderSchema` (the wire contract) in
// shared/schemas/downloads.ts — one source of truth for the OpenAPI document,
// the generated SDKs, the backend, and the frontend. Do not hand-redeclare it.
export type { Downloader } from '../schemas/downloads'
export type DownloadSourceType = 'http' | 'magnet' | 'torrent_url'
export type DownloadTaskStatus =
| 'queued'
| 'assigned'
| 'downloading'
| 'suspended'
| 'pausing'
| 'paused'
| 'interrupted'
| 'uploading'
| 'canceling'
| 'completed'
| 'failed'
| 'canceled'
export type DownloadTaskAction = 'pause' | 'resume' | 'cancel' | 'retry' | 'restart' | 'delete'
export type DownloadTaskRuntimePhase = 'metadata' | 'downloading' | 'uploading' | 'seeding' | 'completed' | 'error'
export type DownloadTaskBillingState = 'none' | 'ok' | 'insufficient_credits'
// `DownloadTask` is inferred from `downloadTaskSchema` (the wire contract) in
// shared/schemas/downloads.ts — one source of truth for the OpenAPI document,
// the generated SDKs, the backend, and the frontend. The sub-interfaces below
// stay as named building blocks the schema mirrors field-for-field.
export type { DownloadTask, DownloadTaskTimeline, DownloadTaskTimelineItem } from '../schemas/downloads'
export interface DownloadTaskSpec {
source: {
type: DownloadSourceType
uri: string
}
destination: {
folder: string
name: string | null
}
labels: {
category: string | null
tags: string[]
}
}
export interface DownloadTaskExecutionStatus {
state: DownloadTaskStatus
attempt: number
assignment: DownloadTaskAssignment | null
progress: DownloadTaskProgress
billing: DownloadTaskBilling
output: DownloadTaskOutput | null
runtime: DownloadTaskRuntime | null
error: DownloadTaskError | null
resolveStartedAt: string | null
resolveCompletedAt: string | null
downloadCompletedAt: string | null
ingestStartedAt: string | null
ingestCompletedAt: string | null
seedingStartedAt: string | null
seedingStoppedAt: string | null
startedAt: string | null
finishedAt: string | null
updatedAt: string
}
export interface DownloadTaskAssignment {
downloaderId: string
assignedAt?: string | null
uploadToken?: string
}
export interface DownloadTaskTransferProgress {
bytes: number
totalBytes?: number | null
bytesPerSecond: number
}
export interface DownloadTaskProgress {
download: DownloadTaskTransferProgress
upload: DownloadTaskTransferProgress
}
export interface DownloadTaskBilling {
state: DownloadTaskBillingState
authorizedBytes: number
chargedBytes: number
chargedCredits: number
}
export interface DownloadTaskOutput {
objectId: string
}
export interface DownloadTaskError {
code?: string | null
message: string | null
}
export interface DownloadTaskTracker {
url: string
status?: string
peers?: number
seeds?: number
leechers?: number
message?: string
}
export interface DownloadTaskPeer {
address: string
client?: string
progress?: number
downloadBps?: number
uploadBps?: number
countryCode?: string
regionCode?: string
}
export interface DownloadTaskFile {
path: string
size: number
completedBytes?: number
selected?: boolean
}
export interface DownloadTaskRuntime {
engine?: DownloaderEngine
state?: string
phase?: DownloadTaskRuntimePhase
message?: string
updatedAt?: string
progress?: DownloadTaskProgress
torrent?: DownloadTaskTorrentRuntime
seeding?: DownloadTaskSeedingRuntime
connections?: number
etaSeconds?: number | null
trackers?: DownloadTaskTracker[]
peers?: DownloadTaskPeer[]
files?: DownloadTaskFile[]
}
export interface DownloadTaskTorrentRuntime {
infoHash?: string
name?: string
seeders?: number
leechers?: number
peers?: number
}
export interface DownloadTaskSeedingRuntime {
enabled?: boolean
active?: boolean
uploadedBytes?: number
uploadBytesPerSecond?: number
ratio?: number
startedAt?: string | null
expiresAt?: string | null
}
export interface ObjectUploadSession {
id: string
objectId: string
// null for a single-PutObject (≤5 GiB) session; set for S3 multipart.
uploadId: string | null
partSize: number
status: 'active' | 'completed' | 'aborted'
expiresAt: string
createdAt: string
updatedAt: string
}
// The upload instructions returned by POST /objects for a file draft: the
// client PUTs each slice to urls[i] (slice i = bytes [i*partSize, …]), reads the
// ETag of each response, then POSTs them to .../completions.
export interface ObjectUploadInstructions {
sessionId: string
partSize: number
urls: string[]
}
export type BackgroundJobStatus = 'queued' | 'running' | 'completed' | 'failed' | 'canceled'
export type KnownBackgroundJobType = 'archive_compress' | 'archive_extract'
export type BackgroundJobType = KnownBackgroundJobType | (string & {})
export interface BackgroundJobProgress {
inputBytes: number
outputBytes: number
processedBytes: number
fileCount: number
currentFilename: string | null
}
export interface BackgroundJob {
id: string
orgId: string
userId: string
type: BackgroundJobType
status: BackgroundJobStatus
targetFolder: string | null
targetPath: string | null
metadata: Record<string, unknown> | null
progress: BackgroundJobProgress
errorMessage: string | null
resultMetadata: Record<string, unknown> | null
retryable: boolean
cancelable: boolean
retriedFromJobId: string | null
createdAt: string
updatedAt: string
startedAt: string | null
finishedAt: string | null
}
import type { ShareKind as _ShareKind } from '../schemas/share'
export type { ShareKind } from '../schemas/share'
// passwordHash is intentionally not part of the shared wire type; it never leaves the server.
export interface Share {
id: string
token: string
kind: _ShareKind
matterId: string
orgId: string
creatorId: string
expiresAt: string | null
downloadLimit: number | null
views: number
downloads: number
status: 'active' | 'revoked'
createdAt: string
}
export interface ShareRecipient {
id: string
shareId: string
recipientUserId: string | null
recipientEmail: string | null
createdAt: string
}
export interface ShareMatter {
name: string
type: string
dirtype: number
}
export interface ShareListItem extends Share {
matter: ShareMatter
recipientCount: number
// Present on received shares: display name of the user who shared it.
creatorName?: string
}
export interface ShareView {
token: string
kind: _ShareKind
status: 'active' | 'revoked'
expiresAt: string | null
downloadLimit: number | null
matter: { name: string; type: string; size: number; isFolder: boolean }
creatorName: string
requiresPassword: boolean
expired: boolean
exhausted: boolean
accessibleByUser: boolean
downloads: number
views: number
rootRef: string
id?: string
matterId?: string
orgId?: string
creatorId?: string
createdAt?: string
recipients?: ShareRecipient[]
}
export type NotificationType = 'share_received' | 'archive_job_completed' | 'archive_job_failed' | 'team_join'
export interface Notification {
id: string
userId: string
type: NotificationType
title: string
body: string
refType: string | null
refId: string | null
metadata: string | null
readAt: string | null
createdAt: string
}
export type AnnouncementStatus = 'draft' | 'published' | 'archived'
export interface Announcement {
id: string
title: string
body: string
status: AnnouncementStatus
priority: number
publishedAt: string | null
createdBy: string
createdAt: string
updatedAt: string
}
export type ActivityActorType = 'user' | 'anonymous' | 'system' | 'downloader'
export interface ActivityEvent {
id: string
orgId: string
userId: string | null
actorType: ActivityActorType
actorRef: string | null
action: string
targetType: string
targetId: string | null
targetName: string
metadata: string | null
createdAt: string
user: {
id: string | null
name: string
image: string | null
}
}
export interface AdminAuditEvent extends ActivityEvent {
orgName: string | null
}
export interface ImageHostingConfig {
orgId: string
customDomain: string | null
cfHostnameId: string | null
domainVerifiedAt: string | null
refererAllowlist: string | null // JSON array of strings; null/empty => allow all
createdAt: string
updatedAt: string
}
export interface IhostConfigResponse {
enabled: boolean
customDomain: string | null
domainVerifiedAt: number | null
domainStatus: 'none' | 'pending' | 'verified'
dnsInstructions: { recordType: string; name: string; target: string } | null
refererAllowlist: string[] | null
createdAt: number | null
}
export type ImageHostingStatus = 'draft' | 'active'
export interface IhostImageDraftResponse {
id: string
token: string
path: string
uploadUrl: string
storageKey: string
}
export interface IhostImageToolResponse {
data: {
url: string
urlAlt: string
markdown: string
html: string
bbcode: string
}
}
export interface ImageHosting {
id: string
orgId: string
token: string
path: string
storageId: string
storageKey: string
size: number
mime: string
width: number | null
height: number | null
status: ImageHostingStatus
accessCount: number
lastAccessedAt: string | null
createdAt: string
}
export type { ChangelogInfo, InstanceInfo } from './instance'
export type { BindingState, LicenseAssertion, LicenseEdition, LicenseFeature, ProFeature } from './licensing'
export type BrandingThemePresetId = 'default' | 'ocean' | 'forest' | 'rose'
export type BrandingThemeMode = 'preset' | 'custom'
export interface BrandingThemeValues {
primary_color: string
primary_foreground: string
canvas_color: string
sidebar_accent_color: string
ring_color: string
}
export interface BrandingThemeConfig {
mode: BrandingThemeMode
preset: BrandingThemePresetId
custom: BrandingThemeValues | null
configured: boolean
}
export const BRANDING_THEME_PRESETS: Record<BrandingThemePresetId, BrandingThemeValues> = {
default: {
primary_color: '#1a73e8',
primary_foreground: '#ffffff',
canvas_color: '#f8fafc',
sidebar_accent_color: '#dbeafe',
ring_color: '#1a73e8',
},
ocean: {
primary_color: '#007c89',
primary_foreground: '#ffffff',
canvas_color: '#effafa',
sidebar_accent_color: '#cffafe',
ring_color: '#0891b2',
},
forest: {
primary_color: '#2f6f4e',
primary_foreground: '#ffffff',
canvas_color: '#f3f8f2',
sidebar_accent_color: '#dcfce7',
ring_color: '#3f8f63',
},
rose: {
primary_color: '#be3455',
primary_foreground: '#ffffff',
canvas_color: '#fff5f7',
sidebar_accent_color: '#ffe4e6',
ring_color: '#e11d48',
},
}
export function isBrandingThemePresetId(value: string): value is BrandingThemePresetId {
return Object.hasOwn(BRANDING_THEME_PRESETS, value)
}
export interface BrandingConfig {
logo_url: string | null
favicon_url: string | null
wordmark_text: string | null
hide_powered_by: boolean
theme: BrandingThemeConfig
}
export type BrandingThemeField =
| 'theme'
| 'theme_mode'
| 'theme_preset'
| 'theme_primary_color'
| 'theme_primary_foreground'
| 'theme_canvas_color'
| 'theme_sidebar_accent_color'
| 'theme_ring_color'
export type BrandingField = 'logo' | 'favicon' | 'wordmark_text' | 'hide_powered_by' | BrandingThemeField