diff --git a/tgui/packages/tgui/interfaces/CrewManifest.jsx b/tgui/packages/tgui/interfaces/CrewManifest.jsx
new file mode 100644
index 00000000000..7b276dbd1f9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CrewManifest.jsx
@@ -0,0 +1,98 @@
+import { classes } from 'common/react';
+
+import { useBackend } from '../backend';
+import { Icon, Section, Table, Tooltip } from '../components';
+import { Window } from '../layouts';
+
+const commandJobs = [
+ 'Head of Personnel',
+ 'Head of Security',
+ 'Chief Engineer',
+ 'Research Director',
+ 'Chief Medical Officer',
+];
+
+export const CrewManifest = (props) => {
+ const {
+ data: { manifest, positions },
+ } = useBackend();
+
+ return (
+
+
+ {Object.entries(manifest).map(([dept, crew]) => (
+
+
+ {Object.entries(crew).map(([crewIndex, crewMember]) => (
+
+
+ {crewMember.name}
+
+
+ {positions[dept].exceptions.includes(crewMember.rank) && (
+
+
+
+ )}
+ {crewMember.rank === 'Captain' && (
+
+
+
+ )}
+ {commandJobs.includes(crewMember.rank) && (
+
+
+
+ )}
+
+
+ {crewMember.rank}
+
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/LanguageMenu.jsx b/tgui/packages/tgui/interfaces/LanguageMenu.jsx
new file mode 100644
index 00000000000..fc5a43feb61
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LanguageMenu.jsx
@@ -0,0 +1,111 @@
+import { useBackend } from '../backend';
+import { Button, LabeledList, Section } from '../components';
+import { Window } from '../layouts';
+
+export const LanguageMenu = (props) => {
+ const { act, data } = useBackend();
+ const {
+ admin_mode,
+ is_living,
+ omnitongue,
+ languages = [],
+ unknown_languages = [],
+ } = data;
+ return (
+
+
+
+
+ {languages.map((language) => (
+
+ {!!is_living && (
+
+ ))}
+
+
+ {!!admin_mode && (
+ act('toggle_omnitongue')}
+ />
+ }
+ >
+
+ {unknown_languages.map((language) => (
+
+ act('grant_language', {
+ language_name: language.name,
+ })
+ }
+ />
+ }
+ >
+ {language.desc} Key: ,{language.key}{' '}
+ {!!language.shadow && '(gained from mob)'}{' '}
+ {language.can_speak ? 'Can speak.' : 'Cannot speak.'}
+
+ ))}
+
+
+ )}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx b/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx
new file mode 100644
index 00000000000..55eae8d2558
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx
@@ -0,0 +1,115 @@
+import { sortBy } from 'common/collections';
+
+import { useBackend } from '../backend';
+import { Box, Button, Flex, ProgressBar, Section, Table } from '../components';
+import { Window } from '../layouts';
+
+const JOB_REPORT_MENU_FAIL_REASON_TRACKING_DISABLED = 1;
+const JOB_REPORT_MENU_FAIL_REASON_NO_RECORDS = 2;
+
+const sortByPlaytime = sortBy(([_, playtime]) => -playtime);
+
+const PlaytimeSection = (props) => {
+ const { playtimes } = props;
+
+ const sortedPlaytimes = sortByPlaytime(Object.entries(playtimes)).filter(
+ (entry) => entry[1],
+ );
+
+ if (!sortedPlaytimes.length) {
+ return 'No recorded playtime hours for this section.';
+ }
+
+ const mostPlayed = sortedPlaytimes[0][1];
+ return (
+
+ {sortedPlaytimes.map(([jobName, playtime]) => {
+ const ratio = playtime / mostPlayed;
+ return (
+
+
+ {jobName}
+
+
+
+
+
+
+ {(playtime / 60).toLocaleString(undefined, {
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1,
+ })}
+ h
+
+
+
+
+
+ );
+ })}
+
+ );
+};
+
+export const TrackedPlaytime = (props) => {
+ const { act, data } = useBackend();
+ const {
+ failReason,
+ jobPlaytimes,
+ specialPlaytimes,
+ exemptStatus,
+ isAdmin,
+ livingTime,
+ ghostTime,
+ adminTime,
+ } = data;
+ return (
+
+
+ {(failReason &&
+ ((failReason === JOB_REPORT_MENU_FAIL_REASON_TRACKING_DISABLED && (
+ This server has disabled tracking.
+ )) ||
+ (failReason === JOB_REPORT_MENU_FAIL_REASON_NO_RECORDS && (
+ You have no records.
+ )))) || (
+
+
+ act('toggle_exempt')}
+ >
+ Job Playtime Exempt
+
+ )
+ }
+ >
+
+
+
+
+ )}
+
+
+ );
+};