Ui for admin. Logout successfull. Admin not reachable from homepage. Neither is logoutpage. Update for Architecture file.

This commit is contained in:
Domonkos
2026-01-28 16:52:47 +01:00
parent 086e5a867d
commit a094a35a8b
9 changed files with 1384 additions and 156 deletions

View File

@@ -1,40 +1,48 @@
VOYAGE Blog Architecture Documentation VOYAGE Blog & Admin Architecture Documentation
======================================== ===============================================
Purpose Purpose
------- -------
This document describes the architecture of the VOYAGE public blog, This document describes the architecture of the VOYAGE public blog
including folder structure, routing, data flow, and rendering logic. and its internal admin system.
A new developer should be able to understand how blog posts are loaded, It covers folder structure, routing, data flow, authentication,
render routes work, and where to extend the system after reading this file. and the admin UI shell.
A new developer should be able to understand:
- how blog posts are loaded and rendered
- how routing works (public + admin)
- how authentication is handled
- where and how to extend the system
High-Level Overview High-Level Overview
------------------- -------------------
The blog is implemented using the **Next.js App Router** with: The VOYAGE public site is implemented using the **Next.js App Router**
inside a monorepo, with authentication delegated to a **Spring Boot backend**.
- File-based routing Key technologies:
- Dynamic route segments - Next.js (App Router, Server Components)
- MDX-based content - Spring Boot (session-based auth)
- A shared public layout - Tailwind CSS (UI styling)
- File-based content (TXT / MD / MDX)
Key principles: Core principles:
- Blog content lives outside the app router (content/posts) - Public content is always accessible
- Routing is derived from folder structure - Admin tools are protected via backend session
- Rendering happens in async server components - No duplicate authentication logic
- Shared UI is colocated in components/ - Clear separation between public site and admin system
Monorepo Context Monorepo Context
---------------- ----------------
This repository is a monorepo containing multiple applications. This repository is a monorepo containing multiple applications.
Relevant app for the blog: Relevant apps:
- apps/public-web → Public website & blog - apps/public-web → Public website, blog, and admin UI
- apps/workspace-api → Spring Boot backend (auth, API, DB)
Other apps (not covered here): Other apps (not covered here):
- workspace-api
- workspace-ui - workspace-ui
@@ -45,32 +53,36 @@ apps/public-web/
├── app/ ├── app/
│ ├── (site)/ → Public site route group │ ├── (site)/ → Public site route group
│ │ ├── layout.tsx → Shared layout (TopBar, globals) │ │ ├── layout.tsx → Shared public layout (TopBar, globals)
│ │ ├── page.tsx → Homepage │ │ ├── page.tsx → Homepage
│ │ ├── about/ │ │ ├── about/
│ │ │ └── page.tsx → Static About page │ │ │ └── page.tsx → Static About page
│ │ ├── blog/ │ │ ├── blog/
│ │ │ ├── layout.tsx → Blog-specific layout (optional) │ │ │ ├── layout.tsx → Blog-specific layout
│ │ │ ├── page.tsx → Blog index (list of posts) │ │ │ ├── page.tsx → Blog index (list of posts)
│ │ │ └── [slug]/ │ │ │ └── [slug]/
│ │ │ └── page.tsx → Dynamic blog post page │ │ │ └── page.tsx → Dynamic blog post page
│ │ └── admin/ │ │ └── admin/
│ │ ── page.tsx → Admin-only page (protected) │ │ ── layout.tsx → Admin auth guard (server-side)
│ │ ├── page.tsx → Admin dashboard (sidebar + header)
│ │ └── posts/
│ │ └── page.tsx → Admin posts manager
│ │ │ │
│ ├── global.css → Global styles │ ├── global.css → Tailwind + global styles
│ └── layout.tsx → Root layout │ └── layout.tsx → Root layout (imports global.css)
├── components/ ├── components/
│ └── shell/ │ └── shell/
│ └── TopBar.tsx → Shared navigation bar │ └── TopBar.tsx → Shared public navigation bar
├── visuals/ ├── visuals/
│ └── ImageSphereSketch.tsx → Creative / visual components │ └── ImageSphereSketch.tsx → Creative / visual components
├── content/ ├── content/
│ └── posts/ │ └── posts/
│ ├── YYYY-MM-DD-title.mdx → Blog post source files │ ├── YY-MM-DD-title.txt → Blog post source files (current)
── ... ── YYYY-MM-DD-title.md → (optional / supported)
│ └── YYYY-MM-DD-title.mdx → (optional / supported)
├── lib/ ├── lib/
│ └── posts.ts → Blog data loading & parsing logic │ └── posts.ts → Blog data loading & parsing logic
@@ -87,125 +99,102 @@ Routing Logic
------------- -------------
Routing is defined entirely by the folder structure. Routing is defined entirely by the folder structure.
Static routes: Public routes:
- / → app/(site)/page.tsx - / → app/(site)/page.tsx
- /about → app/(site)/about/page.tsx - /about → app/(site)/about/page.tsx
- /blog → app/(site)/blog/page.tsx - /blog → app/(site)/blog/page.tsx
Dynamic routes:
- /blog/[slug] → app/(site)/blog/[slug]/page.tsx - /blog/[slug] → app/(site)/blog/[slug]/page.tsx
Admin route: Admin routes:
- /admin → app/(site)/admin/page.tsx (protected) - /admin → Admin dashboard (protected)
- /admin/posts → Admin post manager (protected)
The `[slug]` directory defines a dynamic route parameter Dynamic route parameters:
that is passed to the page component as `params.slug`. - `[slug]` is derived from the filename in `content/posts`
Data Flow (How a Blog Post Is Rendered) Blog Data Flow (Rendering a Post)
--------------------------------------- ---------------------------------
1. User navigates to: 1. User navigates to:
/blog/some-post-slug /blog/some-post-slug
2. Next.js resolves the route: 2. Next.js resolves:
app/(site)/blog/[slug]/page.tsx app/(site)/blog/[slug]/page.tsx
3. The page component receives: 3. Page receives:
params.slug params.slug
4. The page calls: 4. The page calls:
getPostBySlug(slug) from lib/posts.ts getPostBySlug(slug) from lib/posts.ts
5. lib/posts.ts: 5. lib/posts.ts:
- Reads the corresponding MDX file from content/posts - Reads a file from content/posts
- Parses frontmatter metadata (title, date, etc.) - Supports .txt, .md, .mdx
- Strips date prefix from filename
- Returns structured post data - Returns structured post data
6. The page component renders: 6. Page renders:
- Shared layout (TopBar) - Shared public layout
- Post metadata - Post metadata
- MDX content as React components - Parsed content
7. If the slug does not exist: 7. If the slug does not exist:
- notFound() is triggered - notFound() is triggered
Core Files Explained Admin System Overview
------------------- ---------------------
app/(site)/blog/[slug]/page.tsx The admin system is an **internal tool**, not a CMS yet.
--------------------------------
- Async Server Component
- Receives dynamic params (slug)
- Loads post data via lib/posts
- Handles invalid slugs with notFound()
- Renders full blog post view
app/(site)/blog/page.tsx Current capabilities:
------------------------ - Admin dashboard UI
- Blog index page - Post listing / detection
- Loads all posts via lib/posts - Backend session verification
- Renders list / preview of posts - Logout handling
lib/posts.ts Admin UI principles:
------------- - Clean, minimal, internal-tool aesthetic
- Central data access layer for blog content - Sidebar + large system header
- Handles file system access and MDX parsing - No duplicated navigation actions
- Keeps routing and rendering logic clean - Tailwind-based styling
content/posts/*.mdx
-------------------
- Source of truth for blog content
- File name defines the slug
- Frontmatter stores metadata
- Body is rendered as MDX
components/shell/TopBar.tsx
---------------------------
- Shared navigation component
- Used across all public pages
- Ensures consistent layout and navigation
public/blog/visuals/
--------------------
- Static images for blog posts
- Served directly by Next.js
- Referenced in MDX or page components
Admin Authentication & Security Admin Authentication & Security
-------------------------------- --------------------------------
The blog includes an **admin-only section** used for internal tools Authentication is **not handled by Next.js**.
(e.g. editor preview, drafts, future CMS features).
Authentication is **not handled by Next.js**, but delegated to the Instead, it is delegated to the Spring Boot backend (`workspace-api`).
existing Spring Boot backend (`workspace-api`).
Key principles: Key principles:
- Public blog remains fully accessible without login - Single source of truth for auth (Spring)
- Admin routes require a valid backend session
- Session-based authentication (JSESSIONID) - Session-based authentication (JSESSIONID)
- No JWT, no duplicate auth system - No JWT
- No duplicate auth logic in frontend
- Admin routes require a valid backend session
Admin Route Protection (Frontend) Admin Route Protection (Frontend)
--------------------------------- ---------------------------------
Route: Protection is implemented in:
- /admin → app/(site)/admin/page.tsx
Protection strategy: app/(site)/admin/layout.tsx
- Implemented as an **async Server Component guard**
- On each request: Strategy:
- Admin layout is an async Server Component
- On every request:
- Calls backend endpoint `/api/me` - Calls backend endpoint `/api/me`
- Forwards cookies manually - Forwards incoming cookies manually
- If response is 401 → redirect to /login - If response is 401 → redirect to /login
- If response is 200 → render admin UI - If response is 200 → render admin UI
- Optional role check (ROLE_ADMIN)
Important detail: Important technical detail:
- Server-side fetch must forward cookies explicitly - Server-side fetch MUST forward cookies explicitly
- Using headers().get("cookie") (async)
- credentials: "include" is NOT sufficient in Server Components - credentials: "include" is NOT sufficient in Server Components
@@ -215,30 +204,30 @@ Login Flow (End-to-End)
1. User navigates to: 1. User navigates to:
http://localhost:3000/admin http://localhost:3000/admin
2. Admin page fetches: 2. Admin layout fetches:
GET http://localhost:8080/api/me GET http://localhost:8080/api/me
3. If not authenticated: 3. If not authenticated:
- Backend returns 401 - Backend returns 401
- Next.js redirects to /login (frontend route) - Next.js redirects to /login (frontend)
4. Frontend /login page redirects to backend: 4. Frontend /login redirects to backend:
GET http://localhost:8080/login-redirect?redirect=http://localhost:3000/admin GET http://localhost:8080/login-redirect?redirect=http://localhost:3000/admin
5. Backend: 5. Backend:
- Stores redirect target in session - Stores redirect target in session
- Redirects to /login (without query params) - Redirects to /login (no query params)
6. Spring Security default login page is shown 6. Spring Security default login page is shown
7. User submits credentials 7. User submits credentials
8. On successful login: 8. On successful login:
- Custom successHandler reads redirect from session - Custom success handler reads redirect from session
- User is redirected to: - Redirects user back to:
http://localhost:3000/admin http://localhost:3000/admin
9. Admin page loads successfully 9. Admin UI loads successfully
Backend Endpoints Involved Backend Endpoints Involved
@@ -248,7 +237,7 @@ Backend Endpoints Involved
- Returns current authenticated user - Returns current authenticated user
- 200 → logged in - 200 → logged in
- 401 → not authenticated - 401 → not authenticated
- Never redirects (API-safe) - Never redirects
/login /login
- Spring Security default login page - Spring Security default login page
@@ -259,23 +248,43 @@ Backend Endpoints Involved
- Stores redirect target in session - Stores redirect target in session
- Avoids unsupported query params on /login - Avoids unsupported query params on /login
/logout
- Invalidates session
- Clears JSESSIONID cookie
Admin Posts Manager
-------------------
Route:
- /admin/posts
Responsibilities:
- Reads files from content/posts on the server
- Detects available posts (.txt / .md / .mdx)
- Derives slugs from filenames
- Displays debug info (detected paths, files)
- Links to public blog pages
This is intentionally read-only for now.
Why This Architecture Why This Architecture
--------------------- ---------------------
- Single source of truth for authentication (Spring) - Clear separation of concerns
- No duplicate auth logic in frontend - Public content stays simple and fast
- Clean separation: - Admin tools are protected and internal
- Public content → no auth - No auth duplication
- Admin tools → backend session - SSR-safe and production-ready
- Works with SSR and Server Components - Scales cleanly to future CMS features
- Production-ready pattern for multi-app monorepos
How to Extend (Future) How to Extend (Future)
---------------------- ----------------------
- Admin editor UI - Admin post editor UI
- Draft / preview mode - Draft / preview mode
- Role-based admin features - Role-based admin tools
- Publishing workflow
- CMS integration - CMS integration
- Protected preview links - Protected preview links
@@ -283,8 +292,9 @@ How to Extend (Future)
Current Status Current Status
-------------- --------------
- App Router fully set up - App Router fully set up
- Dynamic blog slugs working - Public blog routing stable
- Shared public layout integrated - Content loading via filesystem stable
- MDX content loading stable - Tailwind styling active
- Admin auth flow stable and tested - Admin auth flow stable and tested
- Ready for styling, animations, and feature extensions - Admin dashboard + posts manager implemented
- Ready for further UX polish and feature extensions

View File

@@ -0,0 +1,11 @@
"use client";
export default function LogoutButton() {
const backend = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8080";
return (
<form action={`${backend}/logout`} method="post">
<button type="submit">Logout</button>
</form>
);
}

View File

@@ -0,0 +1,31 @@
import { redirect } from "next/navigation";
import { headers } from "next/headers";
async function cookieHeader() {
const h = await headers(); // ✅ await
return h.get("cookie") ?? "";
}
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const res = await fetch(`${process.env.BACKEND_URL}/api/me`, {
headers: { cookie: await cookieHeader() }, // ✅ await here too
cache: "no-store",
});
if (res.status === 401) redirect("/login");
if (!res.ok) redirect("/login");
const me = await res.json();
const isAdmin =
Array.isArray(me?.authorities) &&
me.authorities.some((a: any) => a.authority === "ROLE_ADMIN");
if (!isAdmin) redirect("/");
return <>{children}</>;
}

View File

@@ -1,38 +1,97 @@
import { redirect } from "next/navigation"; import Link from "next/link";
import { cookies } from "next/headers";
export default async function AdminPage() { export default function AdminPage() {
const cookieHeader = (await cookies()) const backend = process.env.BACKEND_URL ?? "http://localhost:8080";
.getAll()
.map(({ name, value }) => `${name}=${value}`)
.join("; ");
const res = await fetch(`${process.env.BACKEND_URL}/api/me`, {
headers: { cookie: cookieHeader },
cache: "no-store",
redirect: "manual", // IMPORTANT: dont follow to /login
});
// Spring might answer 302 to /login when unauthenticated
if (res.status === 401 || res.status === 302) redirect("/login");
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to load session: ${res.status} ${text.slice(0, 200)}`);
}
const contentType = res.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
// we got HTML (login page) or something else
redirect("/login");
}
const me = await res.json();
return ( return (
<main> <div className="min-h-screen bg-[#F8FAFC] flex gap-10 p-8 font-sans text-slate-900">
<h1>Admin</h1> {/* 1. SINGLE CLEAN SIDEBAR */}
<pre>{JSON.stringify(me, null, 2)}</pre> <aside className="w-64 bg-white border border-slate-200 rounded-2xl flex flex-col sticky top-8 h-[calc(100vh-4rem)]">
</main> <div className="p-8 mb-4">
<div className="text-2xl font-black tracking-tighter text-indigo-600"></div>
</div>
<div className="flex-1" />
{/* SINGLE LOGOUT AT BOTTOM */}
<div className="p-4 border-t border-slate-100">
<form action={`${backend}/logout`} method="post">
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 text-slate-400 hover:text-red-500 hover:bg-red-50 transition-all rounded-lg text-sm font-bold uppercase tracking-wider">
Logout
</button>
</form>
</div>
</aside>
{/* 2. MAIN CONTENT AREA */}
<main className="flex-1 flex flex-col min-w-0 pt-24"> {/* FAT VOYAGE HEADER */}
<header className="px-12 py-10 bg-white border-b border-slate-200 rounded-2xl"> <div className="max-w-5xl">
<Link href="/" className="inline-block">
<h1 className="text-8xl md:text-9xl font-[1000] tracking-tighter text-slate-900 uppercase leading-none hover:opacity-90 transition">
VOYAGE <span className="text-slate-200">OFFICE</span>
</h1>
</Link>
<div className="flex items-center gap-3 mt-6">
<span className="h-2 w-2 rounded-full bg-emerald-400/80"></span>
<p className="text-[10px] font-mono font-semibold text-slate-300 uppercase tracking-[0.25em] truncate">
System Connected: {backend}
</p>
</div>
</div>
</header>
{/* 3. SYMMETRICAL DASHBOARD CONTENT */}
<div className="p-10 max-w-5xl w-full space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Main Action Card */}
<div className="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm flex flex-col justify-between min-h-[200px]">
<div>
<h3 className="text-sm font-black uppercase tracking-widest text-slate-400 mb-1">Content</h3>
<h2 className="text-2xl font-bold text-slate-800">Post Management</h2>
<p className="text-slate-500 mt-2 text-sm leading-relaxed">
Review and edit all current voyage publications.
</p>
</div>
<Link href="/admin/posts" className="mt-6 inline-flex items-center font-bold text-indigo-600 hover:gap-2 transition-all">
Manage Posts
</Link>
</div>
{/* Database Status Card */}
<div className="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm flex flex-col justify-between min-h-[200px]">
<div>
<h3 className="text-sm font-black uppercase tracking-widest text-slate-400 mb-1">Infrastructure</h3>
<h2 className="text-2xl font-bold text-slate-800">SQL Database</h2>
<p className="text-slate-500 mt-2 text-sm leading-relaxed truncate">
Status: Operational at {backend}/h2-console
</p>
</div>
<a href={`${backend}/h2-console`} target="_blank" className="mt-6 inline-flex items-center font-bold text-indigo-600 hover:gap-2 transition-all">
Open Console
</a>
</div>
</div>
{/* Large Summary Box */}
<div className="bg-indigo-600 rounded-3xl p-10 text-white shadow-xl shadow-indigo-200">
<h3 className="text-indigo-200 text-xs font-black uppercase tracking-widest mb-2">Office Overview</h3>
<div className="flex flex-col md:flex-row justify-between items-end gap-6">
<div className="max-w-md">
<p className="text-2xl font-medium leading-tight italic">
"Efficiency is doing things right; effectiveness is doing the right things."
</p>
</div>
<div className="text-right">
<p className="text-4xl font-black">100%</p>
<p className="text-indigo-200 text-xs font-bold uppercase">System Uptime</p>
</div>
</div>
</div>
</div>
</main>
</div>
); );
} }

View File

@@ -0,0 +1,123 @@
import fs from "node:fs";
import path from "node:path";
import Link from "next/link";
type Post = {
file: string;
slug: string;
};
type PostsResult = {
postsDir: string;
tried: string[];
allFiles: string[];
posts: Post[];
};
function resolvePostsDir(): { postsDir: string; tried: string[] } {
const tried: string[] = [];
// 1) When running `npm run dev` inside apps/public-web
const candidate1 = path.join(process.cwd(), "content", "posts");
tried.push(candidate1);
if (fs.existsSync(candidate1)) return { postsDir: candidate1, tried };
// 2) When running Next from monorepo root (process.cwd() == repo root)
const candidate2 = path.join(process.cwd(), "apps", "public-web", "content", "posts");
tried.push(candidate2);
if (fs.existsSync(candidate2)) return { postsDir: candidate2, tried };
// 3) Fallback: try relative to this file (best-effort)
const candidate3 = path.join(process.cwd(), "..", "content", "posts");
tried.push(candidate3);
if (fs.existsSync(candidate3)) return { postsDir: candidate3, tried };
return { postsDir: candidate1, tried };
}
function getPosts(): PostsResult {
const { postsDir, tried } = resolvePostsDir();
if (!fs.existsSync(postsDir)) {
return { postsDir, tried, allFiles: [], posts: [] };
}
const allFiles = fs.readdirSync(postsDir);
const supported = allFiles.filter((f) =>
f.toLowerCase().endsWith(".txt") ||
f.toLowerCase().endsWith(".md") ||
f.toLowerCase().endsWith(".mdx")
);
const posts = supported.map((file) => {
const base = file.replace(/\.(txt|md|mdx)$/i, "");
// Strip date prefix (YY-MM-DD- or YYYY-MM-DD-)
const slug = base
.replace(/^\d{2}-\d{2}-\d{2}-/, "")
.replace(/^\d{4}-\d{2}-\d{2}-/, "");
return { file, slug };
});
return { postsDir, tried, allFiles, posts };
}
export default function AdminPostsPage() {
const { postsDir, tried, allFiles, posts } = getPosts();
return (
<main style={{ maxWidth: 900 }}>
<h1>Posts</h1>
<p style={{ opacity: 0.8 }}>
<Link href="/admin"> Back to Admin</Link>
</p>
<section
style={{
padding: 12,
border: "1px solid #ddd",
borderRadius: 12,
marginBottom: 16,
}}
>
<div>
<strong>postsDir:</strong> {postsDir}
</div>
<div>
<strong>files in folder:</strong> {allFiles.length}
</div>
<div>
<strong>posts detected:</strong> {posts.length}
</div>
<details style={{ marginTop: 8 }}>
<summary>Debug: paths tried</summary>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>{tried.join("\n")}</pre>
</details>
{allFiles.length > 0 && (
<details style={{ marginTop: 8 }}>
<summary>Debug: files seen</summary>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>{allFiles.join("\n")}</pre>
</details>
)}
</section>
{posts.length === 0 ? (
<p>No posts found.</p>
) : (
<ul>
{posts.map((post) => (
<li key={post.file}>
<Link href={`/blog/${post.slug}`}>{post.slug}</Link>
<span style={{ opacity: 0.6 }}> ({post.file})</span>
</li>
))}
</ul>
)}
</main>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1031,6 +1031,7 @@
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1181,6 +1182,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -1925,6 +1927,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -1946,6 +1949,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -1955,6 +1959,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },

View File

@@ -1,6 +0,0 @@
#FileLock
#Tue Jan 27 18:25:29 CET 2026
hostName=macbook-air-von-melika.fritz.box
id=19c007d24ac0d06a483d34679bbb29160d6d33ed895
method=file
server=192.168.178.68\:58736