Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
736ea10
Begin working on the client-side mailbroker management
IzKuipers Jun 1, 2026
43b8301
fix some css stuff with light mode
Blockyheadman Jul 29, 2026
b6f22fa
Merge pull request #282 from ArcOS-Project/blocky-light-mode-fixes
IzKuipers Jul 29, 2026
338e1ad
add email login support (super simple)
Blockyheadman Jul 30, 2026
e066c03
Fix scrollbars + picture-in-picture button in Firefox
IzKuipers Jul 30, 2026
84fd394
Apply scrollbar fix to core apps also
IzKuipers Jul 30, 2026
005dcec
achieve agnostic accelerator arrangement amazingly
Blockyheadman Aug 1, 2026
0d08a28
shit
Blockyheadman Aug 1, 2026
0c02a66
Make light mode even more readable
IzKuipers Aug 1, 2026
9a00f4c
forgot to check focus state
Blockyheadman Aug 1, 2026
ecb62bb
Move the weather applet to the taskbar
IzKuipers Aug 1, 2026
21fdbf2
Merge pull request #289 from ArcOS-Project/blocky-agnostic-accelerato…
IzKuipers Aug 1, 2026
aede51b
Merge remote-tracking branch 'origin/development' into blocky-email-l…
IzKuipers Aug 1, 2026
18416b2
Merge pull request #287 from ArcOS-Project/blocky-email-login
IzKuipers Aug 1, 2026
0392dab
Merge branch 'development' into dev-izk-firefixes
IzKuipers Aug 2, 2026
e88af28
Merge branch 'beta' into development
IzKuipers Aug 2, 2026
e0e3f16
Merge branch 'development' into dev-izk-firefixes
IzKuipers Aug 2, 2026
9fa710e
Merge branch 'development' into izk-mailbroker-admin
IzKuipers Aug 2, 2026
c949219
Some fixes to go with the mergeback
IzKuipers Aug 2, 2026
d054e28
Continue working on the mailbroker manager app
IzKuipers Aug 2, 2026
14c19b9
Finish the editing of templates
IzKuipers Aug 3, 2026
c81796a
Rename MailbrokerApp to MailbrokerManagerApp
IzKuipers Aug 3, 2026
496b1d0
a
IzKuipers Aug 3, 2026
820087b
Finish Mailbroker Manager
IzKuipers Aug 3, 2026
7d3853f
typo
IzKuipers Aug 3, 2026
8ca0bca
Mailbroker key management
IzKuipers Aug 4, 2026
77d1031
Remove unused type
IzKuipers Aug 4, 2026
e623324
Various issues I noticed
IzKuipers Aug 4, 2026
19822f4
Merge pull request #291 from ArcOS-Project/izk-mailbroker-admin
IzKuipers Aug 4, 2026
39230b5
Merge remote-tracking branch 'origin/development' into dev-izk-firefixes
IzKuipers Aug 4, 2026
22ac8fd
Merge pull request #288 from ArcOS-Project/dev-izk-firefixes
IzKuipers Aug 5, 2026
075bfe6
Replace some additional css variables references
IzKuipers Aug 5, 2026
d9003fa
Merge remote-tracking branch 'origin/beta' into development
IzKuipers Aug 6, 2026
af22334
New login wallpaper: img39
IzKuipers Aug 6, 2026
a13d273
Merge pull request #296 from ArcOS-Project/dev-izk-img39-forest-edge
IzKuipers Aug 6, 2026
b66af9a
Merge branch 'beta' into development
IzKuipers Aug 6, 2026
e60f802
Login screen v10: multi-user is back!
IzKuipers Aug 8, 2026
4b3b419
IPswdResetWizard
IzKuipers Aug 9, 2026
d3ee81a
cleanup
IzKuipers Aug 9, 2026
1203e9f
Merge pull request #297 from ArcOS-Project/dev-izk-loginscreen-v10
IzKuipers Aug 9, 2026
92ab297
Begin working on message signatures
IzKuipers Aug 14, 2026
36cbdf6
Bugfixes o' plenty
IzKuipers Aug 16, 2026
c27b4c2
idk what I'm doing
IzKuipers Aug 17, 2026
3539f05
Continue improving sqeleton
IzKuipers Aug 18, 2026
0c519ff
Comment out the signatures button for now
IzKuipers Aug 25, 2026
99365bc
fix bug
IzKuipers Aug 25, 2026
b3be2bf
Merge pull request #300 from ArcOS-Project/dev-izk-message-signatures
IzKuipers Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@types/node": "^22.10.5",
"@types/sql.js": "^1.4.9",
"@types/string-width": "^4.0.1",
"@types/ua-parser-js": "^0.7.39",
"@types/validator": "^13.12.2",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
Expand Down Expand Up @@ -84,7 +85,7 @@
"svelte-image-viewer": "^7.1.0",
"svelte-markdown": "^0.4.1",
"svelte-splitpanes": "^8.0.9",
"ua-parser-js": "^2.0.3",
"ua-parser-js": "1.0.41",
"validator": "^13.12.0",
"vite-plugin-no-bundle": "^4.0.0",
"xterm": "^5.3.0",
Expand Down
57 changes: 57 additions & 0 deletions src/apps/admin/mailbrokermanagerapp/MailbrokerManager.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import type { ICommandResult } from "$interfaces/ICommandResult";
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import CustomTitlebar from "$lib/CustomTitlebar.svelte";
import { CommandResult } from "$ts/result";
import { onMount, type Component } from "svelte";
import Sidebar from "./MailbrokerManager/Sidebar.svelte";
import { mailbrokerPages } from "./store";
import type { MailbrokerPage } from "./types";
import Spinner from "$lib/Spinner.svelte";

let { process }: { process: IMailbrokerManagerRuntime } = $props();
const { currentPage, pageProps } = process;
let Page: Component | undefined = $state();
let pageData = $state<MailbrokerPage>();
let loading = $state<boolean>(false);
let data = $state<ICommandResult<Record<string, any>>>();

onMount(() => {
const sub = currentPage.subscribe(async (v) => {
loading = true;
pageData = mailbrokerPages.get(v);
const canAccess = !pageData?.scopes || process.admin.canAccess(...pageData?.scopes);
data = canAccess
? ((await pageData?.data?.(process, pageProps())) ?? CommandResult.Ok())
: CommandResult.Error("You're missing the required scopes to access this resource.");

Page = pageData?.content;
loading = false;
});

return () => sub();
});
</script>

<Sidebar {process} />
<div class="container">
<CustomTitlebar {process} />
<div class="page-content page-{pageData?.name.toLowerCase().replaceAll(' ', '-')}" class:loading>
{#if loading}
<Spinner height={32} />
{:else if Page && data?.success}
{@const result = data.result!}
<Page {process} data={result} pageProps={pageProps()} {pageData} />
{:else}
<div class="page-load-error">
<div class="error">
<span class="lucide icon-circle-x"></span>
<div>
<h1>The page failed to load</h1>
<p>{data?.errorMessage ?? "An unknown error occurred."}</p>
</div>
</div>
</div>
{/if}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { formatBytes } from "$ts/util/fs";
import type { Mailbroker } from "$types/server/mailbroker";

let { sentRecord }: { sentRecord: Mailbroker.SentMail } = $props();
</script>

<div class="pills">
<div class="pill size">
<span class="lucide icon-code"></span>
<span>{formatBytes(sentRecord.htmlContent.length)}</span>
</div>
<div class="pill size">
<span class="lucide icon-text-align-start"></span>
<span>{formatBytes(sentRecord.textContent.length)}</span>
</div>
{#if sentRecord.to.username && sentRecord.to.serverName}
<div class="pill clr-blue">
<span class="lucide icon-user-round"></span>
<span>To a user</span>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { Server } from "$ts/env";
import type { Mailbroker } from "$types/server/mailbroker";

let { serverKey, disabled = false }: { serverKey: Mailbroker.MailKey; disabled: boolean } = $props();
</script>

<div class="pills">
{#if disabled}
<div class="pill clr-orange">
<span>Disabled</span>
</div>
{:else}
<div class="pill clr-green">
<span>Enabled</span>
</div>
{/if}
{#if serverKey.serverName === "arcapi.nl"}
<div class="pill clr-purple">Master key</div>
{/if}
{#if Server.hostname === serverKey.serverName}
<div class="pill clr-blue">This server</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import { mailbrokerPages } from "../store";

let { process }: { process: IMailbrokerManagerRuntime } = $props();
const { currentPage } = process;
</script>

<div class="window-symbolic-sidebar">
<section class="sidebar-section">
<h1>Mailbroker manager</h1>
{#each [...mailbrokerPages] as [id, page]}
{#if !page.hidden}
<button class="sidebar-button page" class:selected={$currentPage === id} onclick={() => process.switchPage(id)}>
<span class="lucide icon-{page.icon}"></span>
<span>{page.name}</span>
</button>
{#if page.separator}
<hr />
{/if}
{/if}
{/each}
</section>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import { AdminScopes } from "$ts/servicehost/services/AdminBootstrapper/store";
import { Plural } from "$ts/util";
import { Store } from "$ts/writable";
import type { Mailbroker } from "$types/server/mailbroker";
import type { Unsubscriber } from "$types/shared/writable";
import { onDestroy, onMount } from "svelte";
import ListingOption from "./TemplateListing/ListingOption.svelte";

const {
process,
templates,
hideNewButton = false,
}: {
process: IMailbrokerManagerRuntime;
templates: Mailbroker.MailTemplate[];
hideNewButton?: boolean;
} = $props();
const searchValue = Store<string>("");

let filteredTemplates = $state<Mailbroker.MailTemplate[]>(templates);
let unsubscribe: Unsubscriber;

onMount(() => {
unsubscribe = searchValue.subscribe((v) => {
const query = v.toLowerCase().trim();

if (!query) {
filteredTemplates = templates;
return;
}

filteredTemplates = templates.filter((template) => {
if (template.name.toLowerCase().includes(query)) return true;
if (template.fromSuffix.toLowerCase().includes(query)) return true;
if (template.subjectContent.toLowerCase().includes(query)) return true;
if (template.textContent.toLowerCase().includes(query)) return true;
if (template._id.toLowerCase().includes(query)) return true;

return false;
});
});
});

onDestroy(() => {
unsubscribe?.();
});
</script>

<div class="template-listing">
<div class="listing-header">
<h1>{filteredTemplates.length} {Plural("template", filteredTemplates.length)}</h1>
<div class="search-bar">
<span class="lucide icon-search"> </span>
<input type="text" bind:value={$searchValue} placeholder="Search templates" />
</div>
{#if !hideNewButton && process.admin.canAccess(AdminScopes.adminMailbrokerTemplatesWrite)}
<button class="suggested new-template" onclick={() => process.switchPage("newTemplate")}>
<span>New...</span>
</button>
{/if}
</div>

<div class="templates">
{#each filteredTemplates as template (template._id)}
<ListingOption {process} {template} />
{/each}

{#if !filteredTemplates.length}
<p class="empty-notice">
<span>There are no templates that match the criteria.</span>
</p>
{/if}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import type { Mailbroker } from "$types/server/mailbroker";
import TemplatePills from "../TemplatePills.svelte";

let { template, process }: { template: Mailbroker.MailTemplate; process: IMailbrokerManagerRuntime } = $props();

async function viewTemplate() {
process.switchPage("viewTemplate", { templateId: template._id });
}
</script>

<button class="template-listing-option" onclick={viewTemplate}>
<span class="lucide icon-book-dashed"></span>
<div class="info">
<h2>{template.name} - ArcOS {template.fromSuffix}</h2>
<p class="subject">Subject: {template.subjectContent}</p>
<TemplatePills {template} deprecated={template.deprecated} />
</div>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { formatBytes } from "$ts/util/fs";
import type { Mailbroker } from "$types/server/mailbroker";

let { template, deprecated }: { template: Mailbroker.MailTemplate; deprecated: boolean; } = $props();
</script>

<div class="pills">
<div class="pill size">
<span class="lucide icon-code"></span>
<span>{formatBytes(template.htmlContent.length)}</span>
</div>
<div class="pill size">
<span class="lucide icon-text-align-start"></span>
<span>{formatBytes(template.textContent.length)}</span>
</div>
{#if deprecated}
<div class="pill clr-orange">Deprecated</div>
{/if}
</div>
44 changes: 44 additions & 0 deletions src/apps/admin/mailbrokermanagerapp/MailbrokerManagerApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import "$css/apps/admin/mailbroker.css";
import type { App } from "$types/apps/app";
import MailbrokerManager from "./MailbrokerManager.svelte";
import { MailbrokerManagerRuntime } from "./runtime";

const MailbrokerManagerApp: App = {
metadata: {
name: "Mailbroker Manager",
version: "1.0.0",
author: "Izaak Kuipers",
appGroup: "adminTools",
icon: "MailbrokerAdminIcon",
},
position: { centered: true },
size: { w: 800, h: 480 },
minSize: { w: 800, h: 480 },
maxSize: { w: NaN, h: NaN },
state: {
maximized: false,
minimized: false,
resizable: true,
fullscreen: false,
headless: true,
},
controls: {
minimize: true,
maximize: true,
close: true,
},
assets: {
runtime: MailbrokerManagerRuntime,
component: MailbrokerManager as any,
},
glass: true,
elevated: true,
hidden: false,
core: false,
overlay: false,
noSafeMode: false,
vital: true,
id: "MailbrokerManagerApp",
};

export default MailbrokerManagerApp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import type { IMailbrokerNewKeyOverlayRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import Icon from "$lib/Icon.svelte";
import ActionBar from "$lib/Window/ActionBar.svelte";
import ActionButton from "$lib/Window/ActionBar/ActionButton.svelte";
import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog";

let { process }: { process: IMailbrokerNewKeyOverlayRuntime } = $props();
let serverName = $state("");

async function create() {
const result = await process.admin.createMailbrokerKey(serverName);

if (!result.success) {
MessageBox(
{
title: "Failed to create server key",
message: `An error occurred while attempting to create the mailbroker key for ${serverName}. ${result.errorMessage ?? "Unknown failure"}`,
image: "ErrorIcon",
sound: "arcos.dialog.warning",
buttons: [BTN_OKAY_SUG],
},
process.parentPid, // the overlay is too small, so use parent
true
);

return;
}

await process.closeWindow();
await process.parent.spawnOverlay("KeyOverlay", result.result!._id);
}
</script>

<div class="top">
<Icon icon="MailbrokerAdminIcon"></Icon>
<div class="right">
<h1>New server key</h1>
<p>Enter the name of the server for which you want to create the server key:</p>
<input type="text" bind:value={serverName} placeholder="*.arcapi.nl" />
</div>
</div>

<ActionBar>
{#snippet rightContent()}
<ActionButton onclick={() => process.closeWindow()}>Cancel</ActionButton>
<ActionButton suggested disabled={!serverName} onclick={create}>Create</ActionButton>
{/snippet}
</ActionBar>
Loading
Loading