Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
732c5ff
include NEXT_PUBLIC_PB_URL in compose
andreasmolnardev Oct 18, 2025
02394f5
docker compose dev: migrations dir
andreasmolnardev Oct 18, 2025
4486cd0
remove search layout
andreasmolnardev Oct 18, 2025
7af867b
jobs: push search items from karakeep to userSearchItems
andreasmolnardev Oct 18, 2025
a1e1752
update default config
andreasmolnardev Oct 18, 2025
74f1141
redesigned genral settings page
andreasmolnardev Oct 18, 2025
de3cf47
redesigned general settings page
andreasmolnardev Oct 18, 2025
b320a42
Few smaller changes and bugfixes, preparation for #1
andreasmolnardev Oct 18, 2025
8bcaeb2
run action on push to dev
andreasmolnardev Oct 18, 2025
0338ccf
Merge pull request #14 from andreasmolnardev/andrew-dev
andreasmolnardev Oct 18, 2025
5c70c88
add sso support
andreasmolnardev Oct 19, 2025
07a7298
update README.md
andreasmolnardev Oct 19, 2025
49bdb17
env vars: add NEXT_PUBLIC prefix
andreasmolnardev Oct 19, 2025
2c00fa7
update version
andreasmolnardev Oct 19, 2025
511705f
Merge pull request #16 from andreasmolnardev/andrew-dev
andreasmolnardev Oct 19, 2025
4b28619
remove redundant console logs
andreasmolnardev Oct 20, 2025
026094a
update docker compose (prod)
andreasmolnardev Oct 20, 2025
83d2bb5
more console logs removed
andreasmolnardev Oct 20, 2025
0d3b22c
karakeep client: improved error handling, connectivity test
andreasmolnardev Oct 20, 2025
792e264
Merge pull request #21 from andreasmolnardev/karakeep-debug
andreasmolnardev Oct 20, 2025
f41c835
feat: old wallpapers are being deleted
m4sc0 Oct 21, 2025
de5f56a
fixed css issues
m4sc0 Oct 21, 2025
55526d2
added import config dialog
m4sc0 Oct 21, 2025
3ad7b4b
Merge pull request #22 from m4sc0/2-wallpaper-single-store
andreasmolnardev Oct 21, 2025
74bac30
change default config
andreasmolnardev Oct 21, 2025
3ba42f7
update .*ignore files
andreasmolnardev Oct 21, 2025
6e13ad0
update docker compose to build using /pocketbase dockerfile
andreasmolnardev Oct 21, 2025
a500853
removed pocketbase from git cache
andreasmolnardev Oct 21, 2025
2a74119
is this the real pocketbase folder now?
andreasmolnardev Oct 21, 2025
e8176f6
add pocketbase
andreasmolnardev Oct 21, 2025
93c3cf9
db: add user acl to delete their uploaded images
andreasmolnardev Oct 21, 2025
478b8c9
update pipeline: build both next/pb docker images
andreasmolnardev Oct 21, 2025
412ec08
Merge pull request #23 from andreasmolnardev/pb_migs
andreasmolnardev Oct 21, 2025
3893580
added PUT /api/v1/config to replace the user config
m4sc0 Oct 21, 2025
e02808e
added upload logic
m4sc0 Oct 21, 2025
3800c88
update pb dockerfile
andreasmolnardev Oct 21, 2025
2021da5
Merge pull request #25 from andreasmolnardev/pb_migs
andreasmolnardev Oct 21, 2025
b12a4a2
update icons
andreasmolnardev Oct 21, 2025
b8602be
change pocketbase build context
andreasmolnardev Oct 21, 2025
642386c
Merge pull request #26 from andreasmolnardev/pb_migs
andreasmolnardev Oct 21, 2025
ca61585
Merge pull request #24 from m4sc0/4-import-config-files
andreasmolnardev Oct 21, 2025
d44add2
Specify docker-compose file in installation instructions
andreasmolnardev Oct 21, 2025
4c5442c
Update README.md
andreasmolnardev Oct 21, 2025
36d4f95
added textarea to modify config on the go
m4sc0 Oct 21, 2025
05b3604
Merge pull request #27 from m4sc0/4-import-config-files
andreasmolnardev Oct 21, 2025
21916b8
signup: password length check
andreasmolnardev Oct 21, 2025
e2b6d0e
assets: add more icon variations
andreasmolnardev Oct 21, 2025
657f5ba
Merge pull request #28 from andreasmolnardev/login-check
andreasmolnardev Oct 21, 2025
2e77924
Update open source projects list in README
andreasmolnardev Oct 21, 2025
400a78c
Add screenshot to README
andreasmolnardev Oct 21, 2025
d587f22
Merge pull request #29 from andreasmolnardev/readme-screenshot
andreasmolnardev Oct 21, 2025
72ed17a
UPDATE pipelines: new release action
andreasmolnardev Oct 22, 2025
311cc8e
Merge pull request #30 from andreasmolnardev/updated-pipelines
andreasmolnardev Oct 22, 2025
5378346
Replace old screenshot with a new one
andreasmolnardev Oct 22, 2025
59332b8
glanceable: fix date format
andreasmolnardev Oct 22, 2025
ec5a3e4
Merge pull request #32 from andreasmolnardev/fix
andreasmolnardev Oct 22, 2025
4c940f6
Update README with disclaimer and feature links
andreasmolnardev Oct 22, 2025
6f97ca4
use stable tag for release instances
andreasmolnardev Oct 22, 2025
086d917
Merge pull request #33 from andreasmolnardev/fix
andreasmolnardev Oct 22, 2025
8c745b3
update version var
andreasmolnardev Oct 22, 2025
11c7e69
Merge pull request #34 from andreasmolnardev/fix
andreasmolnardev Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pocketbase/
17 changes: 13 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name: Build and Deploy API
name: Build Docker Images

on:
push:
branches:
- main
- dev

jobs:
build-and-deploy:
runs-on: ubuntu-latest
runs-on: self-hosted

steps:
- name: Checkout code
Expand All @@ -32,4 +32,13 @@ jobs:
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ secrets.DOCKER_USERNAME }}/dashwise:dev
tags: ${{ secrets.DOCKER_USERNAME }}/dashwise:dev

- name: Build and Push Multi-Arch Pocketbase Docker image
uses: docker/build-push-action@v5
with:
context: ./pocketbase/
file: ./pocketbase/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ secrets.DOCKER_USERNAME }}/dashwise-pb:dev
50 changes: 50 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Build Docker Images (Release)

on:
push:
branches:
- release

jobs:
build-and-deploy:
runs-on: self-hosted

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and Push Multi-Arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKER_USERNAME }}/dashwise:${{ secrets.VERSION }}
${{ secrets.DOCKER_USERNAME }}/dashwise:latest
${{ secrets.DOCKER_USERNAME }}/dashwise:stable

- name: Build and Push Multi-Arch Pocketbase Docker image
uses: docker/build-push-action@v5
with:
context: ./pocketbase/
file: ./pocketbase/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKER_USERNAME }}/dashwise-pb:${{ secrets.VERSION }}
${{ secrets.DOCKER_USERNAME }}/dashwise-pb:latest
${{ secrets.DOCKER_USERNAME }}/dashwise-pb:stable
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/coverage

#pocketbase data
/pocketbase
/pocketbase/pb_data

# next.js
/.next/
Expand Down
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,46 @@
I've been self hosting for a while but did not find a dashboard that suits my needs and that I like the look of.
This is my attempt to solving that.

> **Disclaimer:** This project is still under development. Therefore, only the working features of the app are listed in the features section. Integration with additional services is planned, but will not be available until widget implementation is complete.

## Screenshot
<img width="1445" height="827" alt="Screenshot 2025-10-22 at 08 03 40" src="https://github.com/user-attachments/assets/69061ca0-cba1-4c23-b7bd-59ca691507e0" />

## Features
- Links: store your most important links for quick access
- Glanceables: Bits of one-line information next to the clock
- Spotlight-like Search: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings.
- Integrations: directly integrates with your favourite self hosted apps. For now only Karakeep is supported but more integrations are planned.

## Installation
Grab the docker compose file (docker-compose.prod.yaml), edit env vars, pull, deploy. That's it.

## Configuration
You can use the following environment variables:

| Name | Required | Default Value | Description |
| --- | --- | --- | --- |
| NEXT_PUBLIC_PB_URL | Yes | `http://pocketbase:8090` | URL of the PocketBase instance |
| NEXT_PUBLIC_INTEGRATIONS_ENABLE_SSL | No | `false` | Enable SSL for integrations |
| PB_ADMIN_EMAIL | Yes | `default@dashwise.local` | Email of the PocketBase admin user |
| PB_ADMIN_PASSWORD | Yes | `DashwiseIsAwesome` | Password of the PocketBase admin user |
| NEXT_PUBLIC_APP_URL | Yes | `http://localhost:3000` | URL of the application |
| NEXT_PUBLIC_ENABLE_SSO | No | `false` | Enable Single Sign-On (SSO) |
| NEXT_PUBLIC_DEFAULT_BG_URL | No | `/dashboard-wallpaper.png` | Default background URL |

## Tech Stack
Frontend, API Layer: Next.js
Backend: Pocketbase

## How it works
On signup, a json config is created for each user.
It's available to the frontend via a GET request to /api/v1/config.
Accessing it is handled by the ConfigContext.

## Open Source Projects that make dashwise possible
Self host icons
Nextjs, Shadcn
[Selfh.st icons](https://github.com/selfhst/icons),
[Font Awesome](https://fontawesome.com),
[Nextjs](https://github.com/vercel/next.js), [Shadcn](https://github.com/shadcn-ui/ui)

## Contributions
Feel free to contribute! I'll probably create a roadmap soon.
Feel free to contribute! I'll probably create a more detailed roadmap soon.
13 changes: 13 additions & 0 deletions app/(config-wrapper)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
"use client";
import DashboardLayoutComponent from "@/components/dashboard/DashboardLayout";
import { useEffect } from "react";

export default function DashboardPage() {
useEffect(() => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('pb_token='))
?.split('=')[1];

if (token) {
localStorage.setItem('pb_token', token);
}
}, []);

return (
<DashboardLayoutComponent />
);
Expand Down
9 changes: 3 additions & 6 deletions app/(config-wrapper)/settings/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useRouter } from "next/navigation"
import { DialogDescription } from "@radix-ui/react-dialog"
import ExportConfigDialog from "@/components/settings/ExportConfigDialog"
import { useConfig } from "@/context/ConfigContext"
import ImportConfigDialog from "@/components/settings/ImportConfigDialog.tsx"

export default function AccountSettingsPage() {
const { config } = useConfig();
Expand Down Expand Up @@ -255,14 +256,10 @@ export default function AccountSettingsPage() {
</Dialog>

<h2 className="text-xl col-span-full">Config</h2>
<div className="grid grid-cols-subgrid border border-transparent hover-frosted items-center col-span-full p-1.5 rounded-md">
<FontAwesomeIcon icon={faUpload} />
<p>Import Another Config</p>
<FontAwesomeIcon icon={faCaretRight} />
</div>
<ImportConfigDialog />
<ExportConfigDialog jsonString={JSON.stringify(config)}/>
<h2 className="text-xl col-span-full">Other</h2>

<h2 className="text-xl col-span-full">Other</h2>
<div className="grid grid-cols-subgrid border border-transparent hover-frosted items-center col-span-full p-1.5 rounded-md">
<FontAwesomeIcon icon={faTrash} />
<p>Delete account</p>
Expand Down
13 changes: 6 additions & 7 deletions app/(config-wrapper)/settings/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import config from "@/lib/config";
export default function GeneralSettingsPage() {
return <> <h1 className="text-3xl font-semibold mb-4">General</h1>

<div className="content space-y-2">
<h2 className="text-xl">App Info</h2>
<span>dashwise Version {config.version}</span>
<ul>
<li>Github Repo</li>
<li>Github Issues</li>
<li>Support me on Ko-fi</li>
<div className="content space-y-2 frosted rounded-md p-2 flex flex-col">
<h2 className="text-xl font-semibold col-span-full">App Info</h2>
<div className="flex items-center justify-center gap-5"> <img src="/dashwise-icon.png" className="h-14"/> <span><span className="font-semibold text-center text-2xl">dashwise</span> <br /> Version {config.version}</span></div>
<ul className="col-span-full flex gap-2 justify-center my-2">
<li className="frosted rounded-md px-2 py-1 font-medium min-w-40 text-center"><a href="https://github.com/andreasmolnardev/dashwise-next" className="hover:text-(--primary)">GitHub Repo</a></li>
<li className="frosted rounded-md px-2 py-1 font-medium min-w-40 text-center"><a href="https://github.com/andreasmolnardev/dashwise-next/issues" className="hover:text-(--primary)">GitHub Issues</a></li>
</ul>
</div></>;
}
2 changes: 0 additions & 2 deletions app/(config-wrapper)/settings/links/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,6 @@ export default function LinksSettingsPage() {
setDropIndex(null);
};

console.log(linkGroups)

return (
<>
<h1 className="text-2xl font-semibold mb-4">Links</h1>
Expand Down
57 changes: 57 additions & 0 deletions app/api/v1/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getServerPB } from '@/lib/pb';
import { NextResponse } from 'next/server';
import path from 'path';
import { promises as fs } from 'fs';
import config from '@/lib/config';

export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');

if (!code || !state) {
return NextResponse.json({ error: 'Invalid OAuth callback parameters' }, { status: 400 });
}

const pb = getServerPB();

try {
// Finish OAuth with PocketBase
const authData = await pb
.collection('users')
.authWithOAuth2Code('oidc2', code, state, `${process.env.NEXT_PUBLIC_BASE_URL}/api/v1/auth/callback`);

const user = authData.record;

// --- Check if userConfig exists ---
const configs = await pb.collection('userConfig').getFullList({
filter: `associatedUserId="${user.id}"`,
});

if (configs.length === 0) {
// Load default config and create one
const configPath = path.join(process.cwd(), 'public', 'default-config.json');
const configFile = await fs.readFile(configPath, 'utf-8');
const configJson = JSON.parse(configFile);

await pb.collection('userConfig').create({
associatedUserId: user.id,
config: configJson,
});
}

// --- Store auth token as a cookie so client can move it to localStorage ---
const response = NextResponse.redirect(`${config.app_base_url}/home`);
response.cookies.set('pb_token', authData.token, {
httpOnly: false,
secure: true,
sameSite: 'lax',
path: '/',
});

return response;
} catch (err: any) {
console.error('OAuth error:', err.response || err);
return NextResponse.json({ error: 'OAuth login failed' }, { status: 401 });
}
}
20 changes: 20 additions & 0 deletions app/api/v1/auth/sso/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getServerPB } from '@/lib/pb';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
const provider = new URL(request.url).searchParams.get('provider');


const pb = getServerPB();
const authMethods = (await pb.collection('users').listAuthMethods()) as any;
const selected = authMethods.oauth2.providers?.find((p: any) => p.name === provider);
if (selected) {
return NextResponse.redirect(selected.authUrl);
}
const provider_pocketid = authMethods.oauth2.providers[0];
if (provider_pocketid) {
return NextResponse.redirect(provider_pocketid.authUrl);
}
return NextResponse.json({ error: 'Provider not available' }, { status: 400 });

}
54 changes: 52 additions & 2 deletions app/api/v1/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ export async function PATCH(request: Request) {

config[path] = newItem;

console.log(JSON.stringify(config))

// 4. persist back to PocketBase
await pb
.collection('userConfig')
Expand All @@ -153,4 +151,56 @@ export async function PATCH(request: Request) {
{ status: 500 }
);
}
}

export async function PUT(request: Request) {
try {
// 1) auth
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const token = authHeader.split(' ')[1];
const pb = getServerPB();
pb.authStore.save(token, null);

let authModel;
try {
authModel = await pb.collection('users').authRefresh();
} catch (error) {
if (error instanceof ClientResponseError && error.status === 401) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
throw error;
}

// 2) body
const body = await request.json().catch(() => null) as { config?: unknown } | null;
if (!body || body.config === undefined || body.config === null || typeof body.config !== 'object') {
return NextResponse.json({ error: 'Body must be { "config": { ... } }' }, { status: 400 });
}

// 3) load existing record or create
let record: any | null = null;
try {
record = await pb.collection('userConfig')
.getFirstListItem(`associatedUserId="${authModel.record.id}"`);
} catch (e) {
record = null;
}

if (record) {
await pb.collection('userConfig').update(record.id, { config: body.config });
} else {
await pb.collection('userConfig').create({
associatedUserId: authModel.record.id,
config: body.config,
});
}

return NextResponse.json({ succes: true });
} catch (error) {
console.error("Error replacing config:", error);
return NextResponse.json({ error: 'Failed to replace config' }, { status: 500 });
}
}
9 changes: 0 additions & 9 deletions app/api/v1/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export async function GET(req: NextRequest) {
const topics = await pb.collection("notificationTopics").getFullList({
filter: `userId="${userId}"`,
});
console.log(topics)
const topicIds = topics.map(t => t.id);

if (topicIds.length === 0) {
Expand All @@ -51,12 +50,6 @@ export async function GET(req: NextRequest) {
sort: "-created",
});

// Debug: inspect expand structure for the first item
if (items.length > 0) {
console.log("expand shape (first item):", JSON.stringify(items[0].expand, null, 2));
console.log("raw topicId (first item):", items[0].topicId);
}

if (count) {
return NextResponse.json({
total: items.length,
Expand Down Expand Up @@ -100,8 +93,6 @@ export async function GET(req: NextRequest) {
};
});

console.log("result", result)

return NextResponse.json({ items: result });
} catch (err: any) {
console.error("Error in GET /notifications", err);
Expand Down
Loading
Loading