Remove scheduler connections and meeting extras
Create PR containing updated CHANGELOG.md and release packages to NPM once PR is merged / Release (push) Has been cancelled
Run i18n AI automation / Run i18n (push) Has been cancelled
Next.js Bundle Analysis / analyze (push) Has been cancelled

This commit is contained in:
2026-06-15 15:10:39 -06:00
parent 782a578492
commit e6fc365278
11 changed files with 7 additions and 249 deletions
@@ -1,12 +0,0 @@
import { NextResponse, type NextRequest } from "next/server";
import { requireSchedulerSession } from "@scheduler/lib/scheduler/auth";
import { getConnections } from "@scheduler/lib/scheduler/service";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {
const auth = await requireSchedulerSession(request);
if ("response" in auth) return auth.response;
return NextResponse.json(await getConnections(auth.userId));
}
-23
View File
@@ -1,23 +0,0 @@
import { redirect } from "next/navigation";
import { AppShell } from "@scheduler/components/AppShell";
import { ConnectionsView } from "@scheduler/components/ConnectionsView";
import { getServerViewerId } from "@scheduler/lib/scheduler/auth";
import { getConnections, getTeamContext } from "@scheduler/lib/scheduler/service";
export const dynamic = "force-dynamic";
export default async function ConnectionsPage() {
const viewerId = await getServerViewerId();
if (viewerId === null) {
redirect("/login?callbackUrl=/connections");
}
const [context, { connections }] = await Promise.all([getTeamContext(viewerId), getConnections(viewerId)]);
return (
<AppShell active="connections" user={{ name: context.viewer.name, timeZone: context.viewer.timeZone }}>
<ConnectionsView connections={connections} />
</AppShell>
);
}
+1 -50
View File
@@ -725,8 +725,7 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
AVAILABILITY + SETTINGS
================================================================ */
.availability-layout,
.connections-layout {
.availability-layout {
display: grid;
gap: 16px;
max-width: 600px;
@@ -886,54 +885,6 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
padding-top: 4px;
}
/* ================================================================
CONNECTIONS
================================================================ */
.provider-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 0;
border-top: 1px solid var(--border-subtle);
}
.provider-name {
font-size: 13px;
font-weight: 600;
color: var(--text);
text-transform: capitalize;
}
.provider-status.connected {
font-size: 10px;
color: var(--accent);
font-weight: 700;
font-family: var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--accent-bg);
padding: 4px 10px;
border-radius: 20px;
border: 1px solid var(--accent-border);
}
.browse-button {
display: inline-flex;
margin-top: 18px;
}
.privacy-note {
font-size: 10.5px;
color: var(--text-3);
font-family: var(--font-mono);
letter-spacing: 0.04em;
padding: 14px 0 2px;
border-top: 1px solid var(--border-subtle);
text-align: center;
}
/* ================================================================
RESPONSIVE
================================================================ */
+1 -2
View File
@@ -1,12 +1,11 @@
import Link from "next/link";
type ActiveNavItem = "calendar" | "availability" | "connections";
type ActiveNavItem = "calendar" | "availability";
type NavItem = { id: ActiveNavItem; href: string; label: string };
const navItems: NavItem[] = [
{ id: "calendar", href: "/calendar", label: "Calendar" },
{ id: "availability", href: "/availability", label: "Availability" },
{ id: "connections", href: "/connections", label: "Connections" },
];
export function AppShell({
@@ -92,8 +92,6 @@ export function CalendarWorkspace({
const [slots, setSlots] = useState<MutualSlot[]>(initialSlots);
const [durationMinutes, setDurationMinutes] = useState<number>(30);
const [title, setTitle] = useState("");
const [location, setLocation] = useState("");
const [conferencing, setConferencing] = useState(false);
const [selectedSlotStart, setSelectedSlotStart] = useState<string | null>(null);
const [createState, setCreateState] = useState<CreateState>({ kind: "idle" });
const [loading, setLoading] = useState(false);
@@ -167,8 +165,6 @@ export function CalendarWorkspace({
attendeeIds: selectedAttendeeIds,
start: selectedSlotStart,
durationMinutes,
location: location.trim() || undefined,
conferencing: conferencing ? "provider" : "none",
}),
});
@@ -188,7 +184,7 @@ export function CalendarWorkspace({
} catch {
setCreateState({ kind: "error", message: "Could not create the meeting." });
}
}, [selectedSlotStart, title, selectedAttendeeIds, durationMinutes, location, conferencing]);
}, [selectedSlotStart, title, selectedAttendeeIds, durationMinutes]);
return (
<div className="calendar-layout">
@@ -304,16 +300,6 @@ export function CalendarWorkspace({
</div>
</fieldset>
<label className="field">
<span>Location (optional)</span>
<input value={location} onChange={(event) => setLocation(event.target.value)} placeholder="Room or address" />
</label>
<label className="field-inline">
<input type="checkbox" checked={conferencing} onChange={(event) => setConferencing(event.target.checked)} />
<span>Add conferencing link (optional)</span>
</label>
<div className="selected-slot">
{selectedSlotStart ? (
<span>
@@ -1,75 +0,0 @@
import type { SchedulerConnection } from "@scheduler/lib/scheduler/service";
type ConnectionsViewProps = {
connections: SchedulerConnection[];
};
const CONFERENCING_CATEGORIES = new Set(["conferencing", "video"]);
const CAL_SETTINGS_URL = process.env.CAL_SETTINGS_URL ?? "https://scheduler.internal.vyntehome.com";
function settingsHref(path: string) {
return `${CAL_SETTINGS_URL.replace(/\/$/, "")}${path}`;
}
function providerLabel(connection: SchedulerConnection): string {
return connection.appId ?? connection.type;
}
function ProviderRow({ connection }: { connection: SchedulerConnection }) {
return (
<div className="provider-row">
<span className="provider-name">{providerLabel(connection)}</span>
{connection.connected ? (
<span className="provider-status connected">Connected</span>
) : (
<a className="secondary-button" href={settingsHref("/apps/installed")}>
Connect
</a>
)}
</div>
);
}
export function ConnectionsView({ connections }: ConnectionsViewProps) {
const calendars = connections.filter((connection) => connection.category === "calendar");
const conferencing = connections.filter((connection) => CONFERENCING_CATEGORIES.has(connection.category));
return (
<div className="connections-layout">
<header className="panel-header">
<h1>Connections</h1>
<span className="muted">Reuses every provider supported by Cal.</span>
</header>
<section className="settings-card">
<h2>Calendars</h2>
{calendars.length === 0 ? (
<p className="muted">No calendars connected yet.</p>
) : (
calendars.map((connection) => (
<ProviderRow key={`${connection.type}-${connection.appId}`} connection={connection} />
))
)}
<a className="primary-button browse-button" href={settingsHref("/apps/installed/calendar")}>
Browse calendar providers
</a>
</section>
<section className="settings-card">
<h2>Conferencing</h2>
{conferencing.length === 0 ? (
<p className="muted">No conferencing providers connected yet.</p>
) : (
conferencing.map((connection) => (
<ProviderRow key={`${connection.type}-${connection.appId}`} connection={connection} />
))
)}
<a className="primary-button browse-button" href={settingsHref("/apps/installed/conferencing")}>
Browse conferencing providers
</a>
</section>
<p className="privacy-note">Only availability blocks are visible to teammates.</p>
</div>
);
}
@@ -9,7 +9,8 @@ describe("safeSchedulerCallback", () => {
});
it("normalizes redirects for already-authenticated users", () => {
expect(safeSchedulerCallback("/connections")).toBe("/connections");
expect(safeSchedulerCallback("/connections")).toBe("/calendar");
expect(safeSchedulerCallback("/availability")).toBe("/availability");
expect(safeSchedulerCallback("https://example.com")).toBe("/calendar");
});
});
+2 -1
View File
@@ -1,8 +1,9 @@
const DEFAULT_CALLBACK = "/calendar";
const REMOVED_CALLBACKS = new Set(["/connections"]);
export function safeSchedulerCallback(value: string | string[] | undefined): string {
const candidate = Array.isArray(value) ? value[0] : value;
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) {
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//") || REMOVED_CALLBACKS.has(candidate)) {
return DEFAULT_CALLBACK;
}
return candidate;
-16
View File
@@ -71,22 +71,6 @@ export const demoBusyBlocks: BusyBlock[] = [
},
];
export type DemoConnection = {
type: string;
appId: string;
category: string;
connected: boolean;
};
export const demoConnectionsByUser: Record<number, DemoConnection[]> = {
1: [
{ type: "google_calendar", appId: "google-calendar", category: "calendar", connected: true },
{ type: "google_video", appId: "google-meet", category: "conferencing", connected: true },
],
2: [{ type: "office365_calendar", appId: "office365-calendar", category: "calendar", connected: true }],
3: [],
};
export type DemoMeeting = {
id: number;
uid: string;
-52
View File
@@ -4,11 +4,9 @@ import { computeMutualSlots } from "./availability";
import { collapseDateOverrides, expandDateOverride } from "./overrides";
import {
demoBusyBlocks,
demoConnectionsByUser,
demoMeetings,
demoSchedules,
demoUsers,
type DemoConnection,
} from "./demo-data";
import { isDemoMode } from "./demo-mode";
import { filterBusyBlocksForViewer } from "./privacy";
@@ -304,54 +302,6 @@ export async function updateSchedule(userId: number, body: unknown): Promise<Sch
return loadSchedule(userId);
}
// ---------------------------------------------------------------------------
// Connections (no secrets exposed)
// ---------------------------------------------------------------------------
export type SchedulerConnection = {
type: string;
appId: string | null;
category: string;
connected: boolean;
};
export async function getConnections(
userId: number
): Promise<{ connections: SchedulerConnection[] }> {
if (isDemoMode()) {
const demo: DemoConnection[] = demoConnectionsByUser[userId] ?? [];
return {
connections: demo.map((connection) => ({
type: connection.type,
appId: connection.appId,
category: connection.category,
connected: connection.connected,
})),
};
}
// Only non-secret metadata is selected: `key`/`encryptedKey` are never read.
const credentials = await prisma.credential.findMany({
where: { userId },
select: {
type: true,
appId: true,
invalid: true,
app: { select: { categories: true } },
},
orderBy: { id: "asc" },
});
const connections: SchedulerConnection[] = credentials.map((credential) => ({
type: credential.type,
appId: credential.appId,
category: credential.app?.categories?.[0] ?? "other",
connected: credential.invalid !== true,
}));
return { connections };
}
// ---------------------------------------------------------------------------
// Availability
// ---------------------------------------------------------------------------
@@ -497,8 +447,6 @@ const createMeetingSchema = z.object({
attendeeIds: z.array(z.number().int().positive()).min(1),
start: z.string().datetime(),
durationMinutes: z.number().int().positive(),
location: z.string().optional(),
conferencing: z.enum(["none", "provider"]).optional(),
});
export type CreateMeetingResult =
-2
View File
@@ -52,6 +52,4 @@ export type MeetingDraft = {
attendeeIds: number[];
start: string;
durationMinutes: number;
location?: string;
conferencing?: "none" | "provider";
};