Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Public `/download` page for the Minutia macOS companion. It resolves the latest signed DMG from GitHub Releases at request time (cached hourly) and degrades to a "watch for the release" state until the first build ships, so it never links to a 404. Includes system requirements, an open-source link, and how-it-works steps. The landing page and the in-app companion prompt both point to it.
- Minutia Retro: a free, anonymous, multiplayer retrospective board at `/retro`. Guided 7-phase ritual (Lobby, Reflect, Reveal, Theme, Vote, Discuss, Commit) with realtime presence, a synchronized card-flip Reveal, and a closure bloom. Boards are ephemeral (30-day auto-expiry) and rate-limited.
- Anonymous board operations run through `SECURITY DEFINER` Postgres RPCs (default-deny `retro_*` tables); liveness uses Supabase Realtime broadcast and presence with a snapshot reconcile.
- AI theme-clustering suggestions during the Theme phase (OpenRouter; degrades silently when unconfigured).
Expand Down
2 changes: 1 addition & 1 deletion e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ test.describe("Smoke Tests", () => {
});

test("all public routes return valid responses", async ({ request }) => {
const routes = ["/login"];
const routes = ["/login", "/download"];

for (const route of routes) {
const response = await request.get(route);
Expand Down
194 changes: 194 additions & 0 deletions src/app/download/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowRight, Bell, Download, ExternalLink } from "lucide-react";
import {
DESKTOP_RELEASES_URL,
DESKTOP_REPO_URL,
DESKTOP_REQUIREMENTS,
getDesktopRelease,
type DesktopRelease,
} from "@/lib/desktop-download";

export const metadata: Metadata = {
title: "Minutia for Mac",
description:
"Record your meetings from the menu bar. No bot joins the call. Recaps land in Minutia seconds after you hit stop.",
};

export const revalidate = 300;

const steps = [
{
title: "Install and sign in",
description:
"Download the app and sign in with your Minutia account. It lives in the menu bar, out of your way.",
},
{
title: "Record from the menu bar",
description:
"Start a recording yourself, or let meeting detection prompt you the moment a call begins.",
},
{
title: "Stop and get the recap",
description:
"Hit stop and the recap opens in Minutia seconds later, complete with action items.",
},
];

function DownloadCta({
release,
variant,
}: {
release: DesktopRelease;
variant: "ink" | "accent";
}) {
const base =
"inline-flex items-center gap-2 rounded-md px-6 py-3 text-sm font-medium text-paper transition-colors";
const solid =
variant === "ink"
? "bg-ink hover:bg-ink-2"
: "bg-accent hover:bg-accent-hover";

if (release.available) {
return (
<a href={release.downloadUrl} className={`${base} ${solid}`}>
<Download className="size-4" />
Download for macOS
</a>
);
}

return (
<a
href={DESKTOP_RELEASES_URL}
target="_blank"
rel="noreferrer"
className={`${base} ${solid}`}
>
<Bell className="size-4" />
Watch for the release
</a>
);
}

export default async function DownloadPage() {
const release = await getDesktopRelease();
const requirements = release.available
? `${release.version} · ${DESKTOP_REQUIREMENTS}`
: DESKTOP_REQUIREMENTS;

return (
<main className="min-h-full bg-paper text-ink font-sans">
{/* ─── Hero ─── */}
<section className="mx-auto max-w-3xl px-6 pt-24 pb-20 text-center sm:pt-32 sm:pb-28">
<p className="text-xs font-mono uppercase tracking-wider text-accent mb-5">
Minutia for Mac
</p>
<h1 className="font-display text-4xl font-bold leading-tight tracking-tight text-ink sm:text-6xl">
Record your meetings from the menu bar.
</h1>
<p className="mt-6 text-lg text-ink-2 sm:text-xl">
No bot joins the call. Minutia captures your mic and system audio
right from the menu bar, and the recap lands in your workspace seconds
after you hit stop.
</p>
<div className="mt-10 flex items-center justify-center">
<DownloadCta release={release} variant="ink" />
</div>
<p className="mt-3 text-xs text-ink-4">{requirements}</p>
{!release.available && (
<p className="mt-2 text-xs text-ink-4">
Not shipped yet. Watch the repo and GitHub tells you the moment the
first signed build lands.
</p>
)}
<p className="mt-2 text-xs text-ink-4">
Open source.{" "}
<a
href={DESKTOP_REPO_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-accent hover:text-accent-hover"
>
View it on GitHub
<ExternalLink className="size-3" />
</a>
</p>
</section>

{/* ─── How it works ─── */}
<section className="mx-auto max-w-5xl px-6 py-16 border-t border-rule">
<h2 className="font-display text-2xl font-semibold text-ink sm:text-3xl text-center mb-12">
How it works
</h2>
<div className="grid gap-8 sm:grid-cols-3">
{steps.map((step, i) => (
<div key={step.title} className="flex gap-4">
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent-soft font-mono text-sm text-accent">
{i + 1}
</div>
<div>
<h3 className="font-display text-lg font-semibold text-ink">
{step.title}
</h3>
<p className="mt-1 text-sm text-ink-2">{step.description}</p>
</div>
</div>
))}
</div>
</section>

{/* ─── Final CTA ─── */}
<section className="border-t border-rule">
<div className="mx-auto max-w-3xl px-6 py-20 text-center">
{release.available ? (
<>
<h2 className="font-display text-2xl font-semibold text-ink sm:text-3xl">
Bring your meetings in automatically.
</h2>
<p className="mt-4 text-ink-2">
Sign in with your Minutia account and start capturing in
minutes.
</p>
<div className="mt-8">
<DownloadCta release={release} variant="accent" />
</div>
<p className="mt-3 text-xs text-ink-4">{requirements}</p>
</>
) : (
<>
<h2 className="font-display text-2xl font-semibold text-ink sm:text-3xl">
Start capturing on the web today.
</h2>
<p className="mt-4 text-ink-2">
The Mac companion is on the way. Minutia runs in your browser
right now, and the desktop app slots in the moment it ships.
</p>
<div className="mt-8">
<Link
href="/login?mode=signup"
className="inline-flex items-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-medium text-paper transition-colors hover:bg-accent-hover"
>
Get started free
<ArrowRight className="size-4" />
</Link>
</div>
</>
)}
</div>
</section>

{/* ─── Footer ─── */}
<footer className="border-t border-rule">
<div className="mx-auto max-w-5xl px-6 py-8 flex items-center justify-between">
<Link href="/" className="font-display text-sm font-semibold text-ink">
Minutia
</Link>
<p className="text-xs text-ink-4">
The open-source Outstanding Issues Log. Run it yourself.
</p>
</div>
</footer>
</main>
);
}
10 changes: 10 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ export default function LandingPage() {
<p className="mt-3 text-xs text-ink-4">
No credit card required. Open source.
</p>
<p className="mt-6 text-sm text-ink-3">
On a Mac?{" "}
<Link
href="/download"
className="inline-flex items-center gap-1 font-medium text-accent hover:text-accent-hover"
>
Get the macOS companion
<ArrowRight className="size-3.5" />
</Link>
</p>
</section>

{/* ─── Features ─── */}
Expand Down
6 changes: 6 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "weekly",
priority: 1,
},
{
url: `${siteUrl}/download`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
},
];
}
4 changes: 1 addition & 3 deletions src/components/minutia/companion-install-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import * as React from "react";
import { X, Download } from "lucide-react";

const DISMISS_KEY = "minutia.companion-prompt-dismissed";
const DOWNLOAD_URL =
"https://github.com/shiprite-dev/minutia-desktop/releases/latest";

// Inline nudge shown near the recorder when the signed-in user has no companion
// app checked in yet (companion_last_seen_at is null) and has not dismissed it.
Expand Down Expand Up @@ -37,7 +35,7 @@ export function CompanionInstallPrompt({
For the best experience, download and install the companion app.
</p>
<a
href={DOWNLOAD_URL}
href="/download"
target="_blank"
rel="noreferrer"
className="inline-flex h-9 items-center gap-1.5 rounded-lg bg-accent px-3 text-sm font-medium text-white hover:bg-accent-hover"
Expand Down
73 changes: 73 additions & 0 deletions src/lib/desktop-download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const DESKTOP_REPO = "shiprite-dev/minutia-desktop";

export const DESKTOP_REPO_URL = `https://github.com/${DESKTOP_REPO}`;
export const DESKTOP_RELEASES_URL = `${DESKTOP_REPO_URL}/releases`;

export const DESKTOP_REQUIREMENTS =
"Requires macOS 14.4 or later. Apple silicon and Intel.";

export type DesktopRelease =
| { available: true; version: string; downloadUrl: string; releaseUrl: string }
| { available: false };

type GithubRelease = {
tag_name?: string;
name?: string;
html_url?: string;
draft?: boolean;
assets?: { name?: string; browser_download_url?: string }[];
};

// Resolves the newest published DMG from GitHub Releases at request time. The
// /releases/latest API returns 404 until the first non-draft release ships, so
// 404 is the one expected, silent degradation; every other failure is logged so
// a broken resolver cannot masquerade as "no release yet". The 5-minute cache
// bounds both release freshness and how long a transient failure can pin the
// coming-soon state.
export async function getDesktopRelease(): Promise<DesktopRelease> {
try {
const token = process.env.GITHUB_TOKEN;
const res = await fetch(
`https://api.github.com/repos/${DESKTOP_REPO}/releases/latest`,
{
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
next: { revalidate: 300 },
},
);
if (res.status === 404) return { available: false };
if (!res.ok) {
console.error(
`[desktop-download] GitHub releases/latest ${res.status} (ratelimit-remaining=${res.headers.get("x-ratelimit-remaining")})`,
);
return { available: false };
}

const data = (await res.json()) as GithubRelease;
if (data.draft) return { available: false };

const dmg = data.assets?.find((a) =>
a.name?.toLowerCase().endsWith(".dmg"),
);
if (!dmg?.browser_download_url) return { available: false };

const version = data.tag_name ?? data.name;
if (!version) {
console.error("[desktop-download] release has a DMG but no tag or name");
return { available: false };
}

return {
available: true,
version,
downloadUrl: dmg.browser_download_url,
releaseUrl: data.html_url ?? DESKTOP_RELEASES_URL,
};
} catch (err) {
console.error("[desktop-download] resolver threw", err);
return { available: false };
}
}
1 change: 1 addition & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export async function middleware(request: NextRequest) {
"/",
"/login",
"/signup",
"/download",
"/accept-invite",
"/auth/callback",
"/reset-password",
Expand Down
Loading