= new Set();
const condensedAnalyticsParsed = condensedAnalytics.map((e) => {
const versionTotal = JSON.parse(e.versionTotal);
Object.keys(versionTotal).forEach((key) => {
if (versionTotal[key] > 0) condensed_versions.add(key);
});
return {
day: e.day,
versionActive: JSON.parse(e.versionActive),
versionTotal,
leagueActive: JSON.parse(e.leagueActive),
leagueTotal: JSON.parse(e.leagueTotal),
localeActive: JSON.parse(e.localeActive),
localeTotal: JSON.parse(e.localeTotal),
themeActive: JSON.parse(e.themeActive),
themeTotal: JSON.parse(e.themeTotal),
};
});
const condensedVersions = Array.from(condensed_versions);
const versionData = {
labels,
datasets: [
...condensedVersions.map((version, idx) => {
return {
fill: true,
label: version + " Active",
data: condensedAnalyticsParsed.map(
(e) => e.versionActive[version] ?? 0,
),
borderColor: `hsla(${calculateVersionColor(idx)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateVersionColor(idx)}, 100%, 50%, 1)`,
};
}),
...condensedVersions.map((version, idx) => {
return {
fill: false,
label: version + " Inactive",
data: condensedAnalytics.map(
(e) =>
(JSON.parse(e.versionTotal)[version] ?? 0) -
(JSON.parse(e.versionActive)[version] ?? 0),
),
borderColor: `hsla(${calculateVersionColor(idx)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateVersionColor(idx)}, 100%, 50%, 0)`,
};
}),
],
};
// Data for the league graph
const leagueData = {
labels,
datasets: [
...leagueList.map((league, idx) => {
return {
fill: true,
label: league + " Active",
data: condensedAnalyticsParsed.map(
(e) => e.leagueActive[league] ?? 0,
),
borderColor: `hsla(${calculateColor(
idx,
league.length,
)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateColor(
idx,
league.length,
)}, 100%, 50%, 1)`,
};
}),
...leagueList.map((league, idx) => {
return {
fill: true,
label: league + " Inactive",
data: condensedAnalyticsParsed.map(
(e) => (e.leagueTotal[league] ?? 0) - (e.leagueActive[league] ?? 0),
),
borderColor: `hsla(${calculateColor(
idx,
league.length,
)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateColor(
idx,
league.length,
)}, 100%, 50%, 0)`,
};
}),
],
};
// Gets the locale data
const localeData = {
labels,
datasets: [
...locales.map((locale, idx) => {
return {
fill: true,
label: locale + " Active",
data: condensedAnalyticsParsed.map(
(e) => e.localeActive[locale] ?? 0,
),
borderColor: `hsla(${calculateColor(
idx,
locale.length,
)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateColor(
idx,
locale.length,
)}, 100%, 50%, 1)`,
};
}),
...locales.map((locale, idx) => {
return {
fill: true,
label: locale + " Inactive",
data: condensedAnalyticsParsed.map(
(e) => (e.localeTotal[locale] ?? 0) - (e.localeActive[locale] ?? 0),
),
borderColor: `hsla(${calculateColor(
idx,
locale.length,
)}, 100%, 50%, 1)`,
backgroundColor: `hsla(${calculateColor(
idx,
locale.length,
)}, 100%, 50%, 0)`,
};
}),
],
};
// Gets the theme data
const darkColor = dark ? 30 : 0;
const lightColor = dark ? 100 : 80;
// const custom
const themeData = {
labels,
datasets: [
{
fill: true,
label: "Dark Active",
data: condensedAnalyticsParsed.map((e) => e.themeActive.dark ?? 0),
borderColor: `hsla(0, 0%, ${darkColor}%, 1)`,
backgroundColor: `hsla(0, 0%, ${darkColor}%, 1)`,
},
{
fill: true,
label: "Light Active",
data: condensedAnalyticsParsed.map((e) => e.themeActive.light ?? 0),
borderColor: `hsla(120, 0%, ${lightColor}%, 1)`,
backgroundColor: `hsla(120, 0%, ${lightColor}%, 1)`,
},
{
fill: true,
label: "Custom Active",
data: condensedAnalyticsParsed.map((e) => e.themeActive.custom ?? 0),
borderColor: theme.palette.secondary.main,
backgroundColor: theme.palette.secondary.main,
},
{
fill: true,
label: "Dark Inactive",
data: condensedAnalyticsParsed.map(
(e) => (e.themeTotal.dark ?? 0) - (e.themeActive.dark ?? 0),
),
borderColor: `hsla(0, 0%, ${darkColor}%, 1)`,
backgroundColor: `hsla(0, 0%, ${darkColor}%, 0)`,
},
{
fill: true,
label: "Light Inactive",
data: condensedAnalyticsParsed.map(
(e) => (e.themeTotal.light ?? 0) - (e.themeActive.light ?? 0),
),
borderColor: `hsla(120, 0%, ${lightColor}%, 1)`,
backgroundColor: `hsla(120, 0%, ${lightColor}%, 0)`,
},
{
fill: true,
label: "Custom Inactive",
data: condensedAnalyticsParsed.map(
(e) => (e.themeTotal.custom ?? 0) - (e.themeActive.custom ?? 0),
),
borderColor: theme.palette.secondary.main,
backgroundColor: `hsla(20, 100%, 50%, 0)`,
},
],
};
// Loads analytics data
useEffect(() => {
let canceled = false;
setTimeout(async () => {
if (canceled || analyticsData.length == 0) {
return;
}
const loadUntilDay =
analyticsData[analyticsData.length - 1].day - graphLength - 10;
let firstLoadedDay = analyticsData[0].day;
while (loadUntilDay < firstLoadedDay && firstDay < firstLoadedDay) {
firstLoadedDay -= 50;
const result = await fetch(
`/api/admin/analytics?day=${firstLoadedDay}`,
);
const data: analytics[] = await result.json();
if (canceled) {
return;
}
setAnalyticsData((prev) => {
if (prev.length == analyticsData.length) {
return [...data, ...prev];
}
return prev;
});
}
}, 100);
return () => {
canceled = true;
};
}, [graphLength, analyticsData, firstDay]);
// Handles when the graph slider changes
function graphLengthChange(e: Event, value: number | number[]) {
if (typeof value === "number") {
setGraphLength(value);
}
}
function graphPrecisionChange(e: Event, value: number | number[]) {
if (typeof value === "number") {
setGraphPrecision(value);
}
}
return (
<>
Version Data
This graph shows how many users are using each (server) version. Active
users are defined as users that are active on that day.
League Type Data
This graph shows how many users are using each league type. Note that
users are counted based on how many leagues they are in.
Locale Data
This graph shows how many users are using which languages.
Theme Data
This graph shows how many users are using dark vs light theme.
Graph Data Length: {graphLength} Days
Maximum points on the Graph: {graphPrecision}
>
);
}
interface configProps extends settingsType {
default_value: string;
}
function Config({
shortName,
name,
default_value,
variant,
options,
}: configProps) {
const [value, setValue] = useState(
variant === "boolean"
? default_value === "1"
? true
: false
: default_value,
);
const notify = useContext(NotifyContext);
const save = async () => {
notify("Saving");
let value2 = value;
if (variant === "textarea") {
setValue(String(value).replace(/\n/g, "").replaceAll(" ", ""));
value2 = String(value).replace(/\n/g, "").replaceAll(" ", "");
}
const res = await fetch("/api/admin/config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: shortName,
value: value2,
}),
});
notify(await res.text(), res.ok ? "success" : "error");
};
switch (variant) {
case "number":
return (
<>
setValue(e.target.value)}
helperText={name}
type="number"
>
Save
>
);
case "select":
return (
<>
{name}
setValue(e.target.value)}
id={shortName}
>
{options?.map((e) => (
{e}
))}
Save
>
);
case "boolean":
return (
<>
setValue(e.target.checked)}
/>
}
label={name}
/>
Save
>
);
case "textarea":
// Automatically parses when pasted in
if (String(value).includes("import")) {
setValue(MUIThemeCodetoJSONString(String(value)));
}
return (
<>
setValue(e.target.value)}
helperText={name}
multiline
fullWidth
>
Save
>
);
default:
return <>>;
}
}
interface props {
analytics: analytics[];
firstDay: number | null;
plugins: plugins[];
pluginData: (store | "error")[];
version: string;
config: data[];
}
export default function Home({
analytics,
firstDay,
plugins,
pluginData,
version,
config,
}: props) {
const [newPlugin, setNewPlugin] = useState("");
const router = useRouter();
const notify = useContext(NotifyContext);
// Used to install a plugin
async function installPlugin() {
const res = await fetch("/api/admin/plugins", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
url: newPlugin,
enabled: false,
settings: "{}",
}),
});
notify(await res.text(), res.ok ? "success" : "error");
router.push("/admin");
}
return (
<>
Admin Panel
Admin Panel
League Plugins
For a league to be useable it needs to be installed and enabled. Leagues
will be installed on a server restart.
setNewPlugin(e.target.value)}
fullWidth
label="Plugin Url"
placeholder="https://raw.githubusercontent.com/"
/>
Install New Plugin
Settings
Information Update Settings
Data is automatically updated following these settings. There is a min
and a max time for during the game and one for the transfer times. The
data is updated when the maximum time is exceeded or a request is
recieved asking for data from the server when the minimum time is
exceeded.
{settings.map((setting) => (
e.value1 === "config" + setting.shortName,
)[0] ?? { value2: "" }
).value2
}
{...setting}
/>
))}
Picture Downloading
This is the setting for how player pictures should be downloaded. No
means they are never downloaded, needed means they are downloaded when
needed, new&needed means they are downloaded when needed and when a new
picture is found, and yes means every picture is downloaded on startup
and downloaded when discovered.
e.value1 === "configDownloadPicture")[0] ?? {
value2: "",
}
).value2
}
name={"Picture Downloading"}
shortName="DownloadPicture"
variant="select"
options={["no", "needed", "new&needed", "yes"]}
/>
Custom Theme
In the textbox below you can paste a custom theme. To create one go to{" "}
MUI Theme Creator.
{" "}
When you are done editing your theme copy it and paste it below, it will
be automatically formatted to the correct format. The first textarea is
the dark theme while the second one is the light theme. If you want to
use the default just enter `{"{}"}` as the value.
{["Dark", "Light"].map((theme) => (
e.value1 === "configTheme" + theme)[0] ?? {
value2: "{}",
}
).value2
}
key={theme}
name={`${theme} Theme`}
shortName={`Theme${theme}`}
variant="textarea"
/>
))}
Analytics
{firstDay !== null && (
)}
{firstDay === null && No Analytics Data Exists
}
>
);
}
export const getServerSideProps: GetServerSideProps = async (
ctx: GetServerSidePropsContext,
) => {
const user = await getServerSession(ctx.req, ctx.res, authOptions);
// Makes sure the user is logged in
if (!user) {
return {
redirect: {
destination: `/api/auth/signin?callbackUrl=${encodeURIComponent(
ctx.resolvedUrl,
)}`,
permanent: false,
},
};
}
if (user.user.admin) {
// Used to find the amount of historical data to get
const analytics = await db
.selectFrom("analytics")
.selectAll()
.where((eb) =>
eb(
"day",
">",
eb
.selectFrom("analytics")
.select(eb.fn.max("day").as("max_day"))
.where(sql`day % 50`, "=", 0),
),
)
.execute();
const firstDay =
(
await db
.selectFrom("analytics")
.select("day")
.orderBy("day", "asc")
.executeTakeFirst()
)?.day ?? 0;
const plugins = await db.selectFrom("plugins").selectAll().execute();
const pluginData: (store | "error")[] = await Promise.all(
plugins.map(async (plugin) => {
const request = await fetch(plugin.url).catch(() => "error");
if (!(request instanceof Response)) {
return "error";
} else {
return await request.json().catch(() => "error");
}
}),
);
const version = (await import("#/package.json")).default.version;
const config = await db
.selectFrom("data")
.selectAll()
.where("value1", "like", "config%")
.execute();
return {
props: {
analytics,
firstDay,
plugins,
pluginData,
version,
config,
},
};
}
return {
notFound: true,
};
};