Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6600076
Fix various sidebar state issues
fccview Jan 29, 2026
40f45f8
actually calculate char width and line height to accurately show line…
fccview Jan 29, 2026
214dd56
add allowed origins to next config
fccview Jan 29, 2026
662ec9b
persist table of content
fccview Jan 31, 2026
7eb65f1
add ruler to markdown editor
fccview Jan 31, 2026
90cbaba
fix circular dependency around sharing items
fccview Feb 4, 2026
603b549
add currentuser to checklist item check
fccview Feb 4, 2026
e242d84
add currentuser to checklist item check
fccview Feb 4, 2026
baae7cd
better fix for circular dependency
fccview Feb 4, 2026
c7613c4
USE GREP TO FIND NOTES/CHECKLISTS BY UUID, HOW THE FUCK DIDNT I THINK…
fccview Feb 4, 2026
5cae523
install grep to dockerfile
fccview Feb 5, 2026
8ca878c
huge improvements by using grep/sed, scary update but very needed
fccview Feb 5, 2026
99c526f
fix archive bug
fccview Feb 6, 2026
1d1233f
safe public sharing
fccview Feb 6, 2026
c8ba0e2
phew upgrade was such a pain
fccview Feb 6, 2026
b003526
fix linting
fccview Feb 6, 2026
2b131c6
fix linting and add offline cache for read mode
fccview Feb 6, 2026
0c58940
fix linting and add offline cache for read mode
fccview Feb 6, 2026
5f6661b
fix kanban reordering
fccview Feb 6, 2026
4cdaeec
fix kanban reordering
fccview Feb 6, 2026
61090e0
finally push all the ws work, terrifying but gotta happen at some poi…
fccview Feb 6, 2026
16f4f26
fix various category issues and improve performance overheads
fccview Feb 7, 2026
6fa8eb1
add swipe gestures to notes on mobile
fccview Feb 7, 2026
8f64109
last additions to swiping
fccview Feb 7, 2026
8490a3a
massively improve sidebar gestures on mobile
fccview Feb 7, 2026
49246d9
update all env variables to use utils check so its consistent across …
fccview Feb 11, 2026
cd9d3e5
fix quick creation to get category from sidebar in case
fccview Feb 11, 2026
a89dafe
fix bottom bar order and sharing issues
fccview Feb 11, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ next-env.d.ts

*.test.yml
.claude

# serwist (auto-generated service worker)
public/sw*
public/swe-worker*
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN apk add --no-cache su-exec git
RUN apk add --no-cache su-exec git grep sed

RUN if ! getent group 1000 > /dev/null 2>&1; then \
addgroup --system --gid 1000 appgroup; \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ A self-hosted app for your checklists and notes.
- **Customisable:** 14 built-in themes plus custom theme support with custom emojis and icons.
- **Encryption:** Full on PGP encryption, read more about it in [howto/ENCRYPTION.md](howto/ENCRYPTION.md)
- **API Access:** Programmatic access to your checklists and notes via REST API with authentication.
- **PWA** Jotty doesn't have a native app, but it's built mobile first. Once installed the PWA on your device it will feel like you installed it from the app store. There's also partial offline caching, as long as you visited a page while online Jotty will allow you to re-visit it while offline. At the moment there's no current support for offline CRUD operation.

<a id="getting-started"></a>

Expand Down
12 changes: 6 additions & 6 deletions app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils";

interface AdminChecklistPageProps {
params: {
params: Promise<{
uuid: string;
};
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: AdminChecklistPageProps): Promise<Metadata> {
export async function generateMetadata(props: AdminChecklistPageProps): Promise<Metadata> {
const params = await props.params;
const { uuid } = params;
return getMedatadaTitle(Modes.CHECKLISTS, uuid, "Admin");
}

export default async function AdminChecklistPage({ params }: AdminChecklistPageProps) {
export default async function AdminChecklistPage(props: AdminChecklistPageProps) {
const params = await props.params;
const { uuid } = params;
const userRecord = await getCurrentUser();
const hasContentAccess = await canAccessAllContent();
Expand Down
12 changes: 6 additions & 6 deletions app/(loggedInRoutes)/admin/note/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";

interface AdminNotePageProps {
params: {
params: Promise<{
uuid: string;
};
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: AdminNotePageProps): Promise<Metadata> {
export async function generateMetadata(props: AdminNotePageProps): Promise<Metadata> {
const params = await props.params;
const { uuid } = params;
return getMedatadaTitle(Modes.NOTES, uuid, "Admin");
}

export default async function AdminNotePage({ params }: AdminNotePageProps) {
export default async function AdminNotePage(props: AdminNotePageProps) {
const params = await props.params;
const { uuid } = params;
const hasContentAccess = await canAccessAllContent();

Expand Down
12 changes: 6 additions & 6 deletions app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils";

interface ChecklistPageProps {
params: {
params: Promise<{
categoryPath: string[];
};
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: ChecklistPageProps): Promise<Metadata> {
export async function generateMetadata(props: ChecklistPageProps): Promise<Metadata> {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
Expand All @@ -37,7 +36,8 @@ export async function generateMetadata({
return getMedatadaTitle(Modes.CHECKLISTS, id, category);
}

export default async function ChecklistPage({ params }: ChecklistPageProps) {
export default async function ChecklistPage(props: ChecklistPageProps) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
Expand Down
12 changes: 6 additions & 6 deletions app/(loggedInRoutes)/howto/[path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import { getTranslations } from "next-intl/server";
import { getHowtoGuideById, getHowtoFilePath, isValidHowtoGuide } from "@/app/_utils/howto-utils";

interface HowtoPageProps {
params: {
params: Promise<{
path: string;
};
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: HowtoPageProps): Promise<Metadata> {
export async function generateMetadata(props: HowtoPageProps): Promise<Metadata> {
const params = await props.params;
const { path } = params;
const t = await getTranslations();
const guide = getHowtoGuideById(path, t);
Expand All @@ -33,7 +32,8 @@ export async function generateMetadata({
};
}

export default async function HowtoPage({ params }: HowtoPageProps) {
export default async function HowtoPage(props: HowtoPageProps) {
const params = await props.params;
const { path } = params;
const t = await getTranslations();

Expand Down
16 changes: 8 additions & 8 deletions app/(loggedInRoutes)/note/[...categoryPath]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";

interface NotePageProps {
params: {
params: Promise<{
categoryPath: string[];
};
}>;
}

export const dynamic = "force-dynamic";

export async function generateMetadata({
params,
}: NotePageProps): Promise<Metadata> {
export async function generateMetadata(props: NotePageProps): Promise<Metadata> {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
Expand All @@ -40,7 +39,8 @@ export async function generateMetadata({
return getMedatadaTitle(Modes.NOTES, id, category);
}

export default async function NotePage({ params }: NotePageProps) {
export default async function NotePage(props: NotePageProps) {
const params = await props.params;
const { categoryPath } = params;
const id = decodeId(categoryPath[categoryPath.length - 1]);
const encodedCategoryPath = categoryPath.slice(0, -1).join("/");
Expand All @@ -55,7 +55,7 @@ export default async function NotePage({ params }: NotePageProps) {
await CheckForNeedsMigration();

const [docsResult, categoriesResult] = await Promise.all([
getUserNotes({ isRaw: true }),
getUserNotes({ isRaw: true, metadataOnly: true }),
getCategories(Modes.NOTES),
]);

Expand All @@ -69,7 +69,7 @@ export default async function NotePage({ params }: NotePageProps) {
const allDocsResult = await getAllNotes();
if (allDocsResult.success && allDocsResult.data) {
note = allDocsResult.data.find(
(doc) => doc.id === id && doc.category === category
(doc) => doc.id === id && doc.category === category,
);
}
}
Expand Down
15 changes: 11 additions & 4 deletions app/(loggedInRoutes)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { getCategories } from "@/app/_server/actions/category";
import { getUserChecklists } from "@/app/_server/actions/checklist";
import { getUserNotes, CheckForNeedsMigration } from "@/app/_server/actions/note";
import {
getUserNotes,
CheckForNeedsMigration,
} from "@/app/_server/actions/note";
import { HomeClient } from "@/app/_components/FeatureComponents/Home/HomeClient";
import { getCurrentUser } from "@/app/_server/actions/users";
import { Modes } from "@/app/_types/enums";
import { Checklist, Note } from "../_types";
import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils";
import { HOMEPAGE_ITEMS_LIMIT } from "@/app/_consts/files";

export const dynamic = "force-dynamic";

Expand All @@ -14,22 +18,25 @@ export default async function HomePage() {

const [listsResult, notesResult, categoriesResult, notesCategoriesResult] =
await Promise.all([
getUserChecklists({ isRaw: true }),
getUserNotes({ isRaw: true }),
getUserChecklists({ limit: HOMEPAGE_ITEMS_LIMIT }),
getUserNotes({ limit: HOMEPAGE_ITEMS_LIMIT }),
getCategories(Modes.CHECKLISTS),
getCategories(Modes.NOTES),
]);

const lists = listsResult.success && listsResult.data ? listsResult.data : [];
const notes = notesResult.success && notesResult.data ? notesResult.data : [];

const categories =
categoriesResult.success && categoriesResult.data
? categoriesResult.data
: [];
const notes = notesResult.success && notesResult.data ? notesResult.data : [];

const notesCategories =
notesCategoriesResult.success && notesCategoriesResult.data
? notesCategoriesResult.data
: [];

const user = sanitizeUserForClient(await getCurrentUser());

return (
Expand Down
13 changes: 10 additions & 3 deletions app/(loggedInRoutes)/settings/connections/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { LinksTab } from "@/app/_components/FeatureComponents/Profile/Parts/LinksTab";
import { readLinkIndex, LinkIndex } from "@/app/_server/actions/link";
import { readLinkIndex } from "@/app/_server/actions/link";
import { LinkIndex } from "@/app/_types";
import { getUsername } from "@/app/_server/actions/users";
import { getArchivedItems } from "@/app/_server/actions/archived";
import { getUserNotes } from "@/app/_server/actions/note";
import { getUserChecklists } from "@/app/_server/actions/checklist";

export default async function ConnectionsPage() {
const username = await getUsername();
const [linkIndex, archivedResult] = await Promise.all([
const [linkIndex, archivedResult, notesResult, checklistsResult] = await Promise.all([
readLinkIndex(username),
getArchivedItems(),
getUserNotes({ username }),
getUserChecklists({ username }),
]);

const archivedItems = archivedResult.success ? archivedResult.data : [];
Expand Down Expand Up @@ -57,6 +62,8 @@ export default async function ConnectionsPage() {
};

const filteredLinkIndex = filterArchivedItems(linkIndex, archivedItems || []);
const notes = notesResult.success ? notesResult.data || [] : [];
const checklists = checklistsResult.success ? checklistsResult.data || [] : [];

return <LinksTab linkIndex={filteredLinkIndex} />;
return <LinksTab linkIndex={filteredLinkIndex} notes={notes} checklists={checklists} />;
}
8 changes: 3 additions & 5 deletions app/(loggedOutRoutes)/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@ import LoginForm from "@/app/_components/GlobalComponents/Auth/LoginForm";
import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell";
import { getTranslations } from "next-intl/server";
import { SsoOnlyLogin } from "@/app/_components/GlobalComponents/Auth/SsoOnlyLogin";
import { isEnvEnabled } from "@/app/_utils/env-utils";

export const dynamic = "force-dynamic";

export default async function LoginPage() {
const t = await getTranslations("auth");
const ssoEnabled = process.env.SSO_MODE === "oidc";
const allowLocal =
process.env.SSO_FALLBACK_LOCAL &&
process.env.SSO_FALLBACK_LOCAL !== "no" &&
process.env.SSO_FALLBACK_LOCAL !== "false";
const allowLocal = isEnvEnabled(process.env.SSO_FALLBACK_LOCAL);

const hasExistingUsers = await hasUsers();
if (!hasExistingUsers && !ssoEnabled || !hasExistingUsers && allowLocal) {
if ((!hasExistingUsers && !ssoEnabled) || (!hasExistingUsers && allowLocal)) {
redirect("/auth/setup");
}

Expand Down
6 changes: 2 additions & 4 deletions app/(loggedOutRoutes)/auth/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import { redirect } from "next/navigation";
import { hasUsers } from "@/app/_server/actions/users";
import SetupForm from "@/app/(loggedOutRoutes)/auth/setup/setup-form";
import { AuthShell } from "@/app/_components/GlobalComponents/Auth/AuthShell";
import { isEnvEnabled } from "@/app/_utils/env-utils";

export const dynamic = "force-dynamic";

export default async function SetupPage() {
const ssoEnabled = process.env.SSO_MODE === "oidc";
const allowLocal =
process.env.SSO_FALLBACK_LOCAL &&
process.env.SSO_FALLBACK_LOCAL !== "no" &&
process.env.SSO_FALLBACK_LOCAL !== "false";
const allowLocal = isEnvEnabled(process.env.SSO_FALLBACK_LOCAL);

if (ssoEnabled && !allowLocal) {
redirect("/auth/login");
Expand Down
Loading