feat(shims): add targeted client-side injections
This commit is contained in:
parent
0771424446
commit
83f203d736
7 changed files with 599 additions and 172 deletions
|
|
@ -20,10 +20,11 @@ import { CommandPalette } from "@/components/command-palette/CommandPalette";
|
|||
import { CommandPaletteProvider } from "@/components/command-palette/CommandPaletteContext";
|
||||
import { KeyboardShortcutsProvider } from "@/components/command-palette/KeyboardShortcutsProvider";
|
||||
import { isSuperuser } from "@/utils/access-control/hasPermission";
|
||||
import { Rank } from "@/payload-types";
|
||||
import { Rank, Shim } from "@/payload-types";
|
||||
import { cookies } from "next/headers";
|
||||
import { IMPERSONATION_ACTIVE_COOKIE } from "@/lib/impersonation";
|
||||
import { ImpersonationBanner } from "@/components/frontend/impersonation/ImpersonationBanner";
|
||||
import { ShimLoader } from "@/components/frontend/shims/ShimLoader";
|
||||
|
||||
// Skip static prerendering of this layout (and all pages under (frontend)/).
|
||||
// The layout calls `getPayload()` at render time to resolve the current user,
|
||||
|
|
@ -67,6 +68,9 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
let isLogistics = false;
|
||||
let currentRank = "";
|
||||
|
||||
const shimsGlobal = await payload.findGlobal({ slug: "shims" });
|
||||
const shimEntries = (shimsGlobal.shims ?? []) as NonNullable<Shim["shims"]>;
|
||||
|
||||
if (user) {
|
||||
[isIntelligence, isLogistics] = await Promise.all([
|
||||
hasIntelligenceQualification(payload, user).catch(() => false),
|
||||
|
|
@ -152,6 +156,11 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
{user && <GameTickRealtime />}
|
||||
{isLogistics && <ShipmentToasts />}
|
||||
<Toaster />
|
||||
<ShimLoader
|
||||
shims={shimEntries}
|
||||
userId={user?.id ?? null}
|
||||
userRoles={user?.roles ?? []}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { default as default_2d468cc5cf0b647ffca3201592c20825 } from '@/component
|
|||
import { default as default_7fa52484aec46e988e9cfb5e86c0c759 } from '@/components/admin/narrative-flow/NarrativeFlowEditor'
|
||||
import { SelectField as SelectField_4490b89d4413c1ffaecdacfe72efaf73 } from '@ai-stack/payloadcms/client'
|
||||
import { PromptEditorField as PromptEditorField_4490b89d4413c1ffaecdacfe72efaf73 } from '@ai-stack/payloadcms/client'
|
||||
import { default as default_b16af798585cda7cfed92f7b1dd0c707 } from '@/components/admin/shims/ShimContentField'
|
||||
import { default as default_2f6c866ab4d4d51b3789f090b84b85c7 } from '@/components/static/VersionOverlay'
|
||||
import { default as default_508743824696caae2894ebf34c2e68bb } from '@/components/graphics/AppIcon'
|
||||
import { default as default_e752fee78378294ff03f91dba0a75a04 } from '@/components/graphics/AppLogo'
|
||||
|
|
@ -34,6 +35,7 @@ export const importMap = {
|
|||
"@/components/admin/narrative-flow/NarrativeFlowEditor#default": default_7fa52484aec46e988e9cfb5e86c0c759,
|
||||
"@ai-stack/payloadcms/client#SelectField": SelectField_4490b89d4413c1ffaecdacfe72efaf73,
|
||||
"@ai-stack/payloadcms/client#PromptEditorField": PromptEditorField_4490b89d4413c1ffaecdacfe72efaf73,
|
||||
"@/components/admin/shims/ShimContentField#default": default_b16af798585cda7cfed92f7b1dd0c707,
|
||||
"@/components/static/VersionOverlay#default": default_2f6c866ab4d4d51b3789f090b84b85c7,
|
||||
"@/components/graphics/AppIcon#default": default_508743824696caae2894ebf34c2e68bb,
|
||||
"@/components/graphics/AppLogo#default": default_e752fee78378294ff03f91dba0a75a04,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { requirePermission } from "@/utils/access-control/hasPermission";
|
|||
export const Shims: GlobalConfig = {
|
||||
slug: "shims",
|
||||
access: {
|
||||
update: requirePermission("shims:update"),
|
||||
read: requirePermission("shims:read"),
|
||||
update: requirePermission("shims:update"),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -17,22 +17,137 @@ export const Shims: GlobalConfig = {
|
|||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
description: "Unique identifier for this shim. Used internally for stable hashing.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "summary",
|
||||
type: "text",
|
||||
admin: {
|
||||
description: "Brief description of what this shim does (for admin reference).",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enabled",
|
||||
type: "checkbox",
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description: "Disabled shims are never injected, regardless of targeting rules.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "CSS", value: "css" },
|
||||
{ label: "JavaScript", value: "js" },
|
||||
],
|
||||
required: true,
|
||||
defaultValue: "css",
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
type: "code",
|
||||
required: true,
|
||||
admin: {
|
||||
components: {
|
||||
Field: "@/components/admin/shims/ShimContentField",
|
||||
},
|
||||
description:
|
||||
"CSS or JS code to inject. CSS is added as a <style> tag; JS is added as an inline <script>.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
type: "select",
|
||||
options: ["css", "js"],
|
||||
required: true,
|
||||
defaultValue: "css",
|
||||
name: "targeting",
|
||||
type: "group",
|
||||
label: "Targeting",
|
||||
admin: {
|
||||
description: "Who should this shim be applied to?",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "audience",
|
||||
type: "select",
|
||||
required: true,
|
||||
defaultValue: "all",
|
||||
options: [
|
||||
{ label: "All Users", value: "all" },
|
||||
{ label: "Percentage of Users", value: "percentage" },
|
||||
{ label: "Specific Users", value: "users" },
|
||||
{ label: "By Role", value: "roles" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "percentage",
|
||||
type: "number",
|
||||
min: 0,
|
||||
max: 100,
|
||||
defaultValue: 50,
|
||||
admin: {
|
||||
condition: (_data, siblingData) => siblingData?.audience === "percentage",
|
||||
description:
|
||||
"Percentage of users (0–100) who receive this shim. Assignment is deterministic per user.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "users",
|
||||
type: "relationship",
|
||||
relationTo: "users",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (_data, siblingData) => siblingData?.audience === "users",
|
||||
description: "Only these specific users will receive this shim.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "roles",
|
||||
type: "select",
|
||||
hasMany: true,
|
||||
options: [
|
||||
{ label: "Guest", value: "guest" },
|
||||
{ label: "User", value: "user" },
|
||||
{ label: "Trusted", value: "trusted" },
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Developer", value: "developer" },
|
||||
],
|
||||
admin: {
|
||||
condition: (_data, siblingData) => siblingData?.audience === "roles",
|
||||
description: "Users with any of these roles will receive this shim.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "paths",
|
||||
type: "group",
|
||||
label: "Path Matching",
|
||||
admin: {
|
||||
description: "Which pages should this shim be active on?",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "matchType",
|
||||
type: "select",
|
||||
required: true,
|
||||
defaultValue: "all",
|
||||
options: [
|
||||
{ label: "All Pages", value: "all" },
|
||||
{ label: "Include Only", value: "include" },
|
||||
{ label: "Exclude", value: "exclude" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "paths",
|
||||
type: "text",
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (_data, siblingData) => siblingData?.matchType !== "all",
|
||||
description:
|
||||
"URL path prefixes to match. E.g. '/logistics' matches /logistics and /logistics/market. Leading slash required.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
30
src/components/admin/shims/ShimContentField.tsx
Normal file
30
src/components/admin/shims/ShimContentField.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import { CodeField, useFormFields } from "@payloadcms/ui";
|
||||
import type { CodeFieldClientComponent } from "payload";
|
||||
|
||||
const languageForShimType = (type: unknown) =>
|
||||
type === "css" ? "css" : "javascript";
|
||||
|
||||
/** Keeps Payload's standard code editor while selecting its language per shim row. */
|
||||
export const ShimContentField: CodeFieldClientComponent = (props) => {
|
||||
const typePath = props.path.replace(/\.content$/, ".type");
|
||||
const shimType = useFormFields(([fields]) => fields[typePath]?.value);
|
||||
const language = languageForShimType(shimType);
|
||||
|
||||
return (
|
||||
<CodeField
|
||||
{...props}
|
||||
key={language}
|
||||
field={{
|
||||
...props.field,
|
||||
admin: {
|
||||
...props.field.admin,
|
||||
language,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShimContentField;
|
||||
56
src/components/frontend/shims/ShimLoader.tsx
Normal file
56
src/components/frontend/shims/ShimLoader.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { isShimActive, type ShimEntry } from "@/lib/shims/evaluate";
|
||||
|
||||
interface ShimLoaderProps {
|
||||
shims: ShimEntry[];
|
||||
userId: number | null;
|
||||
userRoles: string[];
|
||||
}
|
||||
|
||||
const SHIM_ATTR = "data-ptf-shim";
|
||||
|
||||
/**
|
||||
* Client component that evaluates active shims and injects their CSS/JS
|
||||
* into the document <head>. Shims are re-evaluated on route changes.
|
||||
*
|
||||
* Shims are injected as <style data-ptf-shim="name"> (CSS) or
|
||||
* <script data-ptf-shim="name"> (JS) elements. On cleanup or route change,
|
||||
* previously injected elements are removed before new ones are added.
|
||||
*/
|
||||
export function ShimLoader({ shims, userId, userRoles }: ShimLoaderProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Remove any previously injected shims
|
||||
const existing = document.querySelectorAll(`[${SHIM_ATTR}]`);
|
||||
existing.forEach((el) => el.remove());
|
||||
|
||||
// Evaluate and inject active shims
|
||||
for (const shim of shims) {
|
||||
if (!isShimActive(shim, userId, userRoles, pathname)) continue;
|
||||
|
||||
if (shim.type === "css") {
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute(SHIM_ATTR, shim.name);
|
||||
style.textContent = shim.content;
|
||||
document.head.appendChild(style);
|
||||
} else if (shim.type === "js") {
|
||||
const script = document.createElement("script");
|
||||
script.setAttribute(SHIM_ATTR, shim.name);
|
||||
script.textContent = shim.content;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: remove all injected shims on unmount
|
||||
return () => {
|
||||
document.querySelectorAll(`[${SHIM_ATTR}]`).forEach((el) => el.remove());
|
||||
};
|
||||
}, [shims, userId, userRoles, pathname]);
|
||||
|
||||
// No visible output — all work happens in the DOM
|
||||
return null;
|
||||
}
|
||||
153
src/lib/shims/evaluate.ts
Normal file
153
src/lib/shims/evaluate.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* Shims targeting evaluation — pure functions, no server dependencies.
|
||||
*
|
||||
* Determines whether a given shim should be active for a specific user on
|
||||
* a specific path. All functions are side-effect free and safe to call in
|
||||
* client components.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types (mirrors what Payload will generate for the shims global)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ShimAudience = "all" | "percentage" | "users" | "roles";
|
||||
export type ShimPathMatch = "all" | "include" | "exclude";
|
||||
|
||||
export interface ShimTargeting {
|
||||
audience: ShimAudience;
|
||||
/** 0–100, used when audience = "percentage". */
|
||||
percentage?: number | null;
|
||||
/** User IDs, used when audience = "users". */
|
||||
users?: Array<number | { id: number }> | null;
|
||||
/** Role strings, used when audience = "roles". */
|
||||
roles?: string[] | null;
|
||||
}
|
||||
|
||||
export interface ShimPathConfig {
|
||||
matchType: ShimPathMatch;
|
||||
paths?: string[] | null;
|
||||
}
|
||||
|
||||
export interface ShimEntry {
|
||||
name: string;
|
||||
summary?: string | null;
|
||||
enabled?: boolean | null;
|
||||
type: "css" | "js";
|
||||
content: string;
|
||||
targeting: ShimTargeting;
|
||||
paths?: ShimPathConfig | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deterministic hash — djb2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simple string hash → unsigned 32-bit integer.
|
||||
* Deterministic: same input always produces the same output.
|
||||
* Used for stable per-user percentage assignments.
|
||||
*/
|
||||
export function djb2Hash(str: string): number {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value in [0, 100) for a given userId + shimName combination.
|
||||
* Deterministic and uniform enough for percentage rollouts.
|
||||
*/
|
||||
export function hashForUser(userId: number, shimName: string): number {
|
||||
const hash = djb2Hash(`${userId}:${shimName}`);
|
||||
return hash % 100;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path matching
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether the current pathname matches the shim's path config.
|
||||
*
|
||||
* - matchType "all" → always matches.
|
||||
* - matchType "include" → pathname must start with one of the listed prefixes.
|
||||
* - matchType "exclude" → pathname must NOT start with any listed prefix.
|
||||
*/
|
||||
export function matchesPath(pathname: string, pathConfig?: ShimPathConfig | null): boolean {
|
||||
if (!pathConfig || pathConfig.matchType === "all") return true;
|
||||
|
||||
const prefixes = pathConfig.paths ?? [];
|
||||
if (prefixes.length === 0) return true;
|
||||
|
||||
const matched = prefixes.some((prefix) => {
|
||||
const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
||||
return pathname === normalized || pathname.startsWith(`${normalized}/`);
|
||||
});
|
||||
|
||||
return pathConfig.matchType === "include" ? matched : !matched;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audience targeting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a user is in the shim's target audience.
|
||||
* Does NOT check enabled or path — call matchesPath() and check enabled separately.
|
||||
*/
|
||||
export function matchesAudience(
|
||||
shim: ShimEntry,
|
||||
userId: number | null,
|
||||
userRoles: string[],
|
||||
): boolean {
|
||||
if (!shim.targeting) return true;
|
||||
|
||||
const { audience } = shim.targeting;
|
||||
|
||||
switch (audience) {
|
||||
case "all":
|
||||
return true;
|
||||
|
||||
case "percentage": {
|
||||
if (userId === null) return false;
|
||||
const threshold = shim.targeting.percentage ?? 0;
|
||||
return hashForUser(userId, shim.name) < threshold;
|
||||
}
|
||||
|
||||
case "users": {
|
||||
if (userId === null) return false;
|
||||
const targetUsers = shim.targeting.users ?? [];
|
||||
return targetUsers.some((u) => (typeof u === "object" ? u.id : u) === userId);
|
||||
}
|
||||
|
||||
case "roles": {
|
||||
const targetRoles = shim.targeting.roles ?? [];
|
||||
return targetRoles.some((role) => userRoles.includes(role));
|
||||
}
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full evaluation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns true if the shim should be active for this user on this path.
|
||||
* Checks: enabled → audience targeting → path matching.
|
||||
*/
|
||||
export function isShimActive(
|
||||
shim: ShimEntry,
|
||||
userId: number | null,
|
||||
userRoles: string[],
|
||||
pathname: string,
|
||||
): boolean {
|
||||
if (!shim.enabled) return false;
|
||||
if (!matchesAudience(shim, userId, userRoles)) return false;
|
||||
if (!matchesPath(pathname, shim.paths)) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1118,171 +1118,10 @@ export interface Campaign {
|
|||
* Background image for the campaign selector card on the campaigns page. Renders translucent with hover effects. Recommended aspect ratio: 16:9 or square.
|
||||
*/
|
||||
coverImage?: (number | null) | Media;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-vehicles".
|
||||
*/
|
||||
export interface GameVehicle {
|
||||
id: number;
|
||||
/**
|
||||
* Display name or callsign for this vehicle instance.
|
||||
* User who created this campaign. Set automatically on create.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The vehicle type/template this instance is based on.
|
||||
*/
|
||||
type: number | Vehicle;
|
||||
/**
|
||||
* The structure where this vehicle is currently stationed. Null if in transit.
|
||||
*/
|
||||
deployedAt?: (number | null) | GameStructure;
|
||||
/**
|
||||
* The faction that owns this vehicle.
|
||||
*/
|
||||
faction?: (number | null) | Faction;
|
||||
status: 'idle' | 'assigned' | 'in_transit' | 'damaged' | 'destroyed';
|
||||
/**
|
||||
* Current fuel level. Must be between 0 and the vehicle type's fuel capacity.
|
||||
*/
|
||||
currentFuel: number;
|
||||
/**
|
||||
* Current health. Must be between 0 and the vehicle type's max health.
|
||||
*/
|
||||
currentHealth: number;
|
||||
storedResources?:
|
||||
| {
|
||||
resource: number | Resource;
|
||||
amount: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
rotated?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Items displaced by grid resizing.
|
||||
*/
|
||||
voidStorage?:
|
||||
| {
|
||||
resource: number | Resource;
|
||||
amount: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
rotated?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-npcs".
|
||||
*/
|
||||
export interface GameNpc {
|
||||
id: number;
|
||||
/**
|
||||
* The NPC's name as shown in-game and on the frontend.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* What this NPC does (e.g. 'Weapons Dealer', 'Field Medic').
|
||||
*/
|
||||
occupation?: string | null;
|
||||
/**
|
||||
* Where this NPC is stationed in-game.
|
||||
*/
|
||||
location?: (number | null) | GameStructure;
|
||||
/**
|
||||
* The faction this NPC belongs to, if any.
|
||||
*/
|
||||
faction?: (number | null) | Faction;
|
||||
/**
|
||||
* A short background/profile for this NPC.
|
||||
*/
|
||||
bio?: string | null;
|
||||
/**
|
||||
* True for NPCs auto-created by the market tick to cover unstocked goods. They are placeholders until included in the game.
|
||||
*/
|
||||
isGenerated?: boolean | null;
|
||||
/**
|
||||
* Whether this NPC is officially part of the game. Generated NPCs start off; flip this on to include them.
|
||||
*/
|
||||
isInGame?: boolean | null;
|
||||
vendor?: {
|
||||
/**
|
||||
* Whether this NPC runs a market stall. Auto-generated vendor stock is attributed to them.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
/**
|
||||
* Multiplies the auto-generated price for this NPC's stock (1 = base market price).
|
||||
*/
|
||||
priceModifier?: number | null;
|
||||
/**
|
||||
* The goods this NPC sells. The market tick prefers these NPCs when stocking those assets.
|
||||
*/
|
||||
sells?: (number | Asset)[] | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-event-logs".
|
||||
*/
|
||||
export interface GameEventLog {
|
||||
id: number;
|
||||
/**
|
||||
* System-generated events are created automatically. Uncheck for manual/GM narrative entries.
|
||||
*/
|
||||
system?: boolean | null;
|
||||
timestamp: string;
|
||||
type: string;
|
||||
message: string;
|
||||
actor?: (number | null) | User;
|
||||
structure?: (number | null) | GameStructure;
|
||||
/**
|
||||
* Which collection the event primarily relates to.
|
||||
*/
|
||||
targetCollection?:
|
||||
| (
|
||||
| 'game-structures'
|
||||
| 'game-vehicles'
|
||||
| 'game-npcs'
|
||||
| 'structures'
|
||||
| 'resources'
|
||||
| 'assets'
|
||||
| 'vehicles'
|
||||
| 'factions'
|
||||
| 'missions'
|
||||
| 'campaigns'
|
||||
| 'users'
|
||||
| 'technologies'
|
||||
| 'maps'
|
||||
| 'shipments'
|
||||
| 'bank-accounts'
|
||||
| 'bank-transactions'
|
||||
| 'ledger-entries'
|
||||
| 'locker-storages'
|
||||
| 'loadouts'
|
||||
| 'market-listings'
|
||||
| 'market-negotiations'
|
||||
| 'tickets'
|
||||
)
|
||||
| null;
|
||||
targetId?: number | null;
|
||||
data?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
owner?: (number | null) | User;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
|
@ -1454,6 +1293,7 @@ export interface Role {
|
|||
| 'campaigns:read'
|
||||
| 'campaigns:update'
|
||||
| 'campaigns:delete'
|
||||
| 'campaigns:manage'
|
||||
| 'factions:create'
|
||||
| 'factions:read'
|
||||
| 'factions:update'
|
||||
|
|
@ -1594,6 +1434,171 @@ export interface Role {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-vehicles".
|
||||
*/
|
||||
export interface GameVehicle {
|
||||
id: number;
|
||||
/**
|
||||
* Display name or callsign for this vehicle instance.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The vehicle type/template this instance is based on.
|
||||
*/
|
||||
type: number | Vehicle;
|
||||
/**
|
||||
* The structure where this vehicle is currently stationed. Null if in transit.
|
||||
*/
|
||||
deployedAt?: (number | null) | GameStructure;
|
||||
/**
|
||||
* The faction that owns this vehicle.
|
||||
*/
|
||||
faction?: (number | null) | Faction;
|
||||
status: 'idle' | 'assigned' | 'in_transit' | 'damaged' | 'destroyed';
|
||||
/**
|
||||
* Current fuel level. Must be between 0 and the vehicle type's fuel capacity.
|
||||
*/
|
||||
currentFuel: number;
|
||||
/**
|
||||
* Current health. Must be between 0 and the vehicle type's max health.
|
||||
*/
|
||||
currentHealth: number;
|
||||
storedResources?:
|
||||
| {
|
||||
resource: number | Resource;
|
||||
amount: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
rotated?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Items displaced by grid resizing.
|
||||
*/
|
||||
voidStorage?:
|
||||
| {
|
||||
resource: number | Resource;
|
||||
amount: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
rotated?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-npcs".
|
||||
*/
|
||||
export interface GameNpc {
|
||||
id: number;
|
||||
/**
|
||||
* The NPC's name as shown in-game and on the frontend.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* What this NPC does (e.g. 'Weapons Dealer', 'Field Medic').
|
||||
*/
|
||||
occupation?: string | null;
|
||||
/**
|
||||
* Where this NPC is stationed in-game.
|
||||
*/
|
||||
location?: (number | null) | GameStructure;
|
||||
/**
|
||||
* The faction this NPC belongs to, if any.
|
||||
*/
|
||||
faction?: (number | null) | Faction;
|
||||
/**
|
||||
* A short background/profile for this NPC.
|
||||
*/
|
||||
bio?: string | null;
|
||||
/**
|
||||
* True for NPCs auto-created by the market tick to cover unstocked goods. They are placeholders until included in the game.
|
||||
*/
|
||||
isGenerated?: boolean | null;
|
||||
/**
|
||||
* Whether this NPC is officially part of the game. Generated NPCs start off; flip this on to include them.
|
||||
*/
|
||||
isInGame?: boolean | null;
|
||||
vendor?: {
|
||||
/**
|
||||
* Whether this NPC runs a market stall. Auto-generated vendor stock is attributed to them.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
/**
|
||||
* Multiplies the auto-generated price for this NPC's stock (1 = base market price).
|
||||
*/
|
||||
priceModifier?: number | null;
|
||||
/**
|
||||
* The goods this NPC sells. The market tick prefers these NPCs when stocking those assets.
|
||||
*/
|
||||
sells?: (number | Asset)[] | null;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "game-event-logs".
|
||||
*/
|
||||
export interface GameEventLog {
|
||||
id: number;
|
||||
/**
|
||||
* System-generated events are created automatically. Uncheck for manual/GM narrative entries.
|
||||
*/
|
||||
system?: boolean | null;
|
||||
timestamp: string;
|
||||
type: string;
|
||||
message: string;
|
||||
actor?: (number | null) | User;
|
||||
structure?: (number | null) | GameStructure;
|
||||
/**
|
||||
* Which collection the event primarily relates to.
|
||||
*/
|
||||
targetCollection?:
|
||||
| (
|
||||
| 'game-structures'
|
||||
| 'game-vehicles'
|
||||
| 'game-npcs'
|
||||
| 'structures'
|
||||
| 'resources'
|
||||
| 'assets'
|
||||
| 'vehicles'
|
||||
| 'factions'
|
||||
| 'missions'
|
||||
| 'campaigns'
|
||||
| 'users'
|
||||
| 'technologies'
|
||||
| 'maps'
|
||||
| 'shipments'
|
||||
| 'bank-accounts'
|
||||
| 'bank-transactions'
|
||||
| 'ledger-entries'
|
||||
| 'locker-storages'
|
||||
| 'loadouts'
|
||||
| 'market-listings'
|
||||
| 'market-negotiations'
|
||||
| 'tickets'
|
||||
)
|
||||
| null;
|
||||
targetId?: number | null;
|
||||
data?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ranks".
|
||||
|
|
@ -3663,6 +3668,7 @@ export interface CampaignsSelect<T extends boolean = true> {
|
|||
startDate?: T;
|
||||
endDate?: T;
|
||||
coverImage?: T;
|
||||
owner?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
|
@ -4530,10 +4536,51 @@ export interface Shim {
|
|||
id: number;
|
||||
shims?:
|
||||
| {
|
||||
/**
|
||||
* Unique identifier for this shim. Used internally for stable hashing.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Brief description of what this shim does (for admin reference).
|
||||
*/
|
||||
summary?: string | null;
|
||||
content: string;
|
||||
/**
|
||||
* Disabled shims are never injected, regardless of targeting rules.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
type: 'css' | 'js';
|
||||
/**
|
||||
* CSS or JS code to inject. CSS is added as a <style> tag; JS is added as an inline <script>.
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* Who should this shim be applied to?
|
||||
*/
|
||||
targeting: {
|
||||
audience: 'all' | 'percentage' | 'users' | 'roles';
|
||||
/**
|
||||
* Percentage of users (0–100) who receive this shim. Assignment is deterministic per user.
|
||||
*/
|
||||
percentage?: number | null;
|
||||
/**
|
||||
* Only these specific users will receive this shim.
|
||||
*/
|
||||
users?: (number | User)[] | null;
|
||||
/**
|
||||
* Users with any of these roles will receive this shim.
|
||||
*/
|
||||
roles?: ('guest' | 'user' | 'trusted' | 'admin' | 'developer')[] | null;
|
||||
};
|
||||
/**
|
||||
* Which pages should this shim be active on?
|
||||
*/
|
||||
paths: {
|
||||
matchType: 'all' | 'include' | 'exclude';
|
||||
/**
|
||||
* URL path prefixes to match. E.g. '/logistics' matches /logistics and /logistics/market. Leading slash required.
|
||||
*/
|
||||
paths?: string[] | null;
|
||||
};
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
|
|
@ -4566,8 +4613,23 @@ export interface ShimsSelect<T extends boolean = true> {
|
|||
| {
|
||||
name?: T;
|
||||
summary?: T;
|
||||
content?: T;
|
||||
enabled?: T;
|
||||
type?: T;
|
||||
content?: T;
|
||||
targeting?:
|
||||
| T
|
||||
| {
|
||||
audience?: T;
|
||||
percentage?: T;
|
||||
users?: T;
|
||||
roles?: T;
|
||||
};
|
||||
paths?:
|
||||
| T
|
||||
| {
|
||||
matchType?: T;
|
||||
paths?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
|
|
|
|||
Loading…
Reference in a new issue