diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index a9ae934..f3f7032 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -1,19 +1,5 @@
name: Build and publish Docker image
-# Builds SaraView's image (Dockerfile at the repo root) and pushes it to
-# GitHub Container Registry (ghcr.io) so it can be pulled directly on
-# ZimaOS instead of building locally every time.
-#
-# Runs on every push to main (tagged "latest" + the short commit SHA) and
-# on version tags like v1.0.0 (tagged with that version). Also runnable
-# by hand from the Actions tab ("workflow_dispatch").
-#
-# One manual, one-time step after this first runs: GHCR packages built via
-# a workflow's built-in token default to PRIVATE, even on a public repo.
-# Go to your GitHub profile -> Packages -> saraview -> Package settings ->
-# Change visibility -> Public, or ZimaOS won't be able to pull it without
-# a login.
-
on:
push:
branches: ["main"]
@@ -32,9 +18,6 @@ jobs:
uses: actions/checkout@v4
- name: Compute lowercase image name
- # GHCR requires an all-lowercase image path even though GitHub
- # usernames/repos themselves are case-insensitive (e.g. SRHTNIN
- # becomes srhtnin here).
run: echo "IMAGE=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
- name: Log in to GitHub Container Registry
diff --git a/Backend/Auth.py b/Backend/Auth.py
index e2af48e..73471c2 100644
--- a/Backend/Auth.py
+++ b/Backend/Auth.py
@@ -10,9 +10,6 @@ from Groups import AdminGroupName, GetGroupsForUser, IsUserInGroup
from Users import GetUser
-# In-memory only for now: sessions don't survive a container restart, so
-# everyone just logs in again. Fine for a handful of trusted tailnet users;
-# worth revisiting if that stops being true.
ActiveSessions: dict[str, str] = {}
SessionCookieName = "SaraViewSession"
@@ -40,8 +37,6 @@ def GetCurrentUser(request: Request) -> dict[str, Any]:
if User is None:
raise HTTPException(status_code=401, detail="Session user no longer exists.")
- # Admin isn't a stored flag — it's membership in the built-in Admin
- # group, computed live, so it behaves exactly like any other group.
UserGroups = GetGroupsForUser(Username)
return {
diff --git a/Backend/Blame.py b/Backend/Blame.py
index 399bd28..dbb4b42 100644
--- a/Backend/Blame.py
+++ b/Backend/Blame.py
@@ -17,10 +17,6 @@ def GetBlameDirectory() -> Path:
def GetBlamePath(RootName: str, RelativePath: str) -> Path:
- # Mirrors the file's own relative path under the Blame/ folder, keyed by
- # root so files with the same relative path in different roots don't
- # collide. Validity of RelativePath itself is checked by the caller via
- # Core.ResolvePath before this is ever used.
return GetBlameDirectory() / RootName / f"{RelativePath}.blame.toml"
@@ -45,10 +41,6 @@ def SaveBlame(RootName: str, RelativePath: str, Blame: dict[str, dict[str, Any]]
def RenameUserInBlame(OldUsername: str, NewUsername: str) -> None:
- """Rewrite every recorded blame attribution from OldUsername to
- NewUsername across every file's blame history — called when a user
- renames their account, so past attributions stay coherent.
- """
BlameDirectory = GetBlameDirectory()
if not BlameDirectory.is_dir():
@@ -74,10 +66,6 @@ def RenameUserInBlame(OldUsername: str, NewUsername: str) -> None:
def UpdateBlameOnSave(
RootName: str, RelativePath: str, OldContent: str, NewContent: str, Username: str
) -> None:
- """Recompute line attribution after a save. Unchanged lines (matched via
- the same diff algorithm git's own blame uses) keep their prior author and
- timestamp; changed or new lines are stamped with the current user & now.
- """
OldLines = OldContent.splitlines()
NewLines = NewContent.splitlines()
PreviousBlame = LoadBlame(RootName, RelativePath)
diff --git a/Backend/BookReader.py b/Backend/BookReader.py
index 19a722a..1398db3 100644
--- a/Backend/BookReader.py
+++ b/Backend/BookReader.py
@@ -15,10 +15,6 @@ CachedSaraTagCoreLoaded = False
def GetSaraTagCore() -> ModuleType | None:
- """Load SaraTag's Core.py directly by file path, under a private module
- name, so it never collides with SaraView's own Core module in sys.modules.
- Cached after the first successful load.
- """
global CachedSaraTagCore, CachedSaraTagCoreLoaded
if CachedSaraTagCoreLoaded:
@@ -54,9 +50,6 @@ def GetSaraTagCore() -> ModuleType | None:
def ReadBookForFile(FilePath: Path) -> dict[str, Any] | None:
- """Return a file's resolved Book (tag values + Lexicon definitions), or
- None if SaraTag integration is disabled or the file has no Book.
- """
SaraTagCore = GetSaraTagCore()
if SaraTagCore is None:
diff --git a/Backend/Core.py b/Backend/Core.py
index 22b4952..23181b8 100644
--- a/Backend/Core.py
+++ b/Backend/Core.py
@@ -12,7 +12,6 @@ ConfigPath = ApplicationDirectory / "SaraViewConf.toml"
def LoadConfig() -> dict[str, Any]:
- """Load SaraView's TOML configuration from disk."""
if not ConfigPath.is_file():
raise FileNotFoundError(f"Missing configuration file: {ConfigPath}")
@@ -24,7 +23,6 @@ Config = LoadConfig()
def GetRoots() -> dict[str, Path]:
- """Return the configured named roots as resolved, absolute paths."""
RawRoots = Config.get("Roots", {})
Roots: dict[str, Path] = {}
@@ -35,10 +33,6 @@ def GetRoots() -> dict[str, Path]:
def ResolvePath(RootName: str, RelativePath: str) -> Path:
- """Resolve a client-supplied relative path against a named root.
-
- Raises ValueError if the root is unknown or the resolved path escapes it.
- """
Roots = GetRoots()
Root = Roots.get(RootName)
@@ -54,7 +48,6 @@ def ResolvePath(RootName: str, RelativePath: str) -> Path:
def IsInternalPath(EntryPath: Path) -> bool:
- """Return True if an entry should be hidden as a SaraTag internal file."""
SaraTagConfig = Config.get("SaraTag", {})
if not SaraTagConfig.get("HideInternalFiles", True):
diff --git a/Backend/Editor.py b/Backend/Editor.py
index 64376df..83a8ba7 100644
--- a/Backend/Editor.py
+++ b/Backend/Editor.py
@@ -4,5 +4,4 @@ from pathlib import Path
def WriteTextFile(FilePath: Path, Content: str) -> None:
- """Overwrite a text file's contents on disk."""
FilePath.write_text(Content, encoding="utf-8")
diff --git a/Backend/Explorer.py b/Backend/Explorer.py
index a9d82a5..9c48c34 100644
--- a/Backend/Explorer.py
+++ b/Backend/Explorer.py
@@ -34,7 +34,6 @@ FileTypeMap = {
def ClassifyFile(EntryPath: Path) -> str:
- """Return a coarse file-type label used to pick a preview renderer."""
return FileTypeMap.get(EntryPath.suffix.lower(), "unknown")
@@ -43,14 +42,10 @@ DefaultNoteExtension = ".md"
def IsNoteFile(EntryName: str) -> bool:
- """A directory's own "file" - editable/viewable like any other file, but
- hidden from normal listings behind the ShowSvFiles setting.
- """
return EntryName.startswith(NoteFilePrefix)
def FindDirectoryNote(DirectoryPath: Path) -> Path | None:
- """Find a directory's note file, whatever extension it currently has."""
Prefix = f"{NoteFilePrefix}{DirectoryPath.name}."
for Candidate in DirectoryPath.iterdir():
@@ -61,9 +56,6 @@ def FindDirectoryNote(DirectoryPath: Path) -> Path | None:
def EnsureDirectoryNote(DirectoryPath: Path) -> Path:
- """Find a directory's note file, creating one (defaulting to markdown)
- if it doesn't have one yet - e.g. a directory that predates this feature.
- """
Existing = FindDirectoryNote(DirectoryPath)
if Existing is not None:
return Existing
@@ -74,7 +66,6 @@ def EnsureDirectoryNote(DirectoryPath: Path) -> Path:
def GetDirectoryNoteInfo(DirectoryPath: Path) -> dict[str, Any] | None:
- """Read-only lookup for listings - never creates a note."""
NotePath = FindDirectoryNote(DirectoryPath)
if NotePath is None:
@@ -84,9 +75,6 @@ def GetDirectoryNoteInfo(DirectoryPath: Path) -> dict[str, Any] | None:
def RenameDirectoryNote(DirectoryPath: Path, OldDirectoryName: str) -> None:
- """After a directory is renamed/moved, keep its note file's name (which
- embeds the directory's own name) in sync, preserving its extension.
- """
OldPrefix = f"{NoteFilePrefix}{OldDirectoryName}."
for Candidate in DirectoryPath.iterdir():
@@ -97,12 +85,10 @@ def RenameDirectoryNote(DirectoryPath: Path, OldDirectoryName: str) -> None:
def ListRoots() -> list[dict[str, Any]]:
- """Return the configured roots as a flat list for the frontend."""
return [{"Name": Name} for Name in GetRoots()]
def CreateEntry(RootName: str, RelativePath: str, IsDirectory: bool) -> None:
- """Create a new empty file or directory at the given path."""
EntryPath = ResolvePath(RootName, RelativePath)
if EntryPath.exists():
@@ -117,9 +103,6 @@ def CreateEntry(RootName: str, RelativePath: str, IsDirectory: bool) -> None:
def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: str) -> None:
- """Rename or move a file/directory within the same root. Used for both
- plain renames (same parent, new name) and full moves.
- """
SourcePath = ResolvePath(RootName, SourceRelativePath)
DestinationPath = ResolvePath(RootName, DestinationRelativePath)
@@ -135,7 +118,6 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
try:
SourcePath.rename(DestinationPath)
except OSError:
- # Cross-device or other rename failures — fall back to a copy+delete.
shutil.move(str(SourcePath), str(DestinationPath))
if DestinationPath.is_dir():
@@ -143,7 +125,6 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
def DeleteEntry(RootName: str, RelativePath: str) -> None:
- """Delete a file, or an entire directory tree."""
EntryPath = ResolvePath(RootName, RelativePath)
if not EntryPath.exists():
@@ -158,15 +139,6 @@ def DeleteEntry(RootName: str, RelativePath: str) -> None:
def ListDirectory(
RootName: str, RelativePath: str, ShowHidden: bool = False, ShowSvFiles: bool = False
) -> list[dict[str, Any]]:
- """List the immediate contents of a directory.
-
- SaraTag's internal files are always hidden, regardless of ShowHidden.
- Note files (.sv_*) are hidden unless ShowSvFiles is True - independently
- of ShowHidden, since they're an implementation detail rather than a
- normal dotfile. Every directory's Note metadata is still attached
- regardless of ShowSvFiles, so clicking the directory can open it.
- Other dotfiles/dotfolders are hidden unless ShowHidden is True.
- """
DirectoryPath = ResolvePath(RootName, RelativePath)
if not DirectoryPath.is_dir():
diff --git a/Backend/Groups.py b/Backend/Groups.py
index 26ffac8..19bc62e 100644
--- a/Backend/Groups.py
+++ b/Backend/Groups.py
@@ -20,9 +20,6 @@ def LoadGroups() -> dict[str, list[str]]:
GroupsPath = GetGroupsPath()
if not GroupsPath.is_file():
- # First run after upgrading from the old boolean-flag admin model:
- # seed the Admin group from whoever already had IsAdmin set, so
- # nobody silently loses admin access.
from Users import LoadUsers
SeededAdmins = [
@@ -90,7 +87,6 @@ def SetGroupMembers(Name: str, Members: list[str]) -> None:
if Name not in Groups:
raise ValueError(f"Group '{Name}' does not exist.")
- # De-duplicate while keeping things predictable.
Groups[Name] = sorted(set(Members))
SaveGroups(Groups)
diff --git a/Backend/Locks.py b/Backend/Locks.py
index c604369..c1d9e71 100644
--- a/Backend/Locks.py
+++ b/Backend/Locks.py
@@ -42,11 +42,8 @@ def GetLockForPath(RootName: str, RelativePath: str) -> dict[str, Any] | None:
def SetLock(RootName: str, RelativePath: str, Usernames: list[str], GroupNames: list[str]) -> None:
- """Restrict a file or directory to specific users and/or groups. Passing
- both empty lists is the same as removing the lock entirely.
- """
if not RelativePath:
- raise ValueError("Can't lock an entire root — pick a specific file or directory.")
+ raise ValueError("Can't lock an entire root - pick a specific file or directory.")
if not Usernames and not GroupNames:
RemoveLock(RootName, RelativePath)
@@ -77,17 +74,6 @@ def RemoveLock(RootName: str, RelativePath: str) -> None:
def IsPathAccessible(
RootName: str, RelativePath: str, Username: str, UserGroups: list[str]
) -> bool:
- """A path is inaccessible if it IS a locked file/directory, or lies
- anywhere underneath a locked directory, and the user isn't permitted
- (by username or group) on every applicable lock. Locks don't propagate
- to subdirectories as separate records — there's only ever one lock
- entry per locked directory — but access to anything inside still
- requires being permitted on that ancestor lock, so you always have to
- "enter the parent first." Admins do NOT bypass locks - Admin is a
- management group (create users, manage groups, and so on), not a way
- to view everyone's private directories. Only direct server/filesystem
- access can see everything.
- """
for Lock in LoadLocks():
if Lock.get("Root") != RootName:
continue
@@ -110,10 +96,6 @@ def IsPathAccessible(
def RequirePathAccessible(RootName: str, RelativePath: str, CurrentUser: dict[str, Any]) -> None:
- """Raises a 404 (never a distinguishing "access denied") for a path the
- current user isn't permitted to see, so a locked directory's existence
- isn't revealed to anyone not on its list.
- """
if not IsPathAccessible(
RootName,
RelativePath,
diff --git a/Backend/Main.py b/Backend/Main.py
index 4fc8393..52d5d23 100644
--- a/Backend/Main.py
+++ b/Backend/Main.py
@@ -80,9 +80,6 @@ Application.add_middleware(
)
-# --- Auth ---------------------------------------------------------------
-
-
class LoginRequest(BaseModel):
Username: str
Password: str
@@ -95,9 +92,6 @@ def AuthStatusEndpoint() -> dict:
@Application.post("/api/auth/bootstrap")
def BootstrapEndpoint(Body: LoginRequest) -> dict:
- """Create the first (admin) account. Only usable while no users exist yet,
- so there's no default/baked-in password sitting in the image.
- """
if AnyUsersExist():
raise HTTPException(status_code=403, detail="Setup already completed.")
@@ -163,9 +157,6 @@ def PostUsersEndpoint(Body: CreateUserRequest, CurrentUser: dict = Depends(Requi
return {"Created": True}
-# --- Profile (colour, avatar, username, password) --------------------------
-
-
@Application.get("/api/profile/colours")
def GetProfileColourOptionsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> list[str]:
return UserColourOptions
@@ -238,10 +229,6 @@ def PutProfileUsernameEndpoint(
RenameUserInBlame(OldUsername, NewUsername)
RenameUserInGroups(OldUsername, NewUsername)
- # Re-authenticate this same request under the new name so the
- # person doesn't get logged out by their own rename. Any OTHER
- # active session for the old name is left to fail naturally on its
- # next request — a re-login there is the simplest correct outcome.
OldToken = request.cookies.get(SessionCookieName)
if OldToken:
DeleteSession(OldToken)
@@ -275,13 +262,8 @@ def PutProfilePasswordEndpoint(
return {"Changed": True}
-# --- Groups -----------------------------------------------------------------
-
-
@Application.get("/api/groups")
def GetGroupsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> list[dict]:
- # Readable by anyone — picking an existing group as a lock target is
- # open to everyone, same as picking a user.
return ListGroups()
@@ -325,13 +307,6 @@ def PutGroupMembersEndpoint(
return {"Saved": True}
-# --- Locked files/directories -----------------------------------------------
-# Setting a lock is open to anyone who currently has access to the path -
-# not admin-only. If you can see it, you're trusted to decide who else can.
-# Admins do NOT bypass locks (see Locks.IsPathAccessible) - Admin is a
-# management group, not a way to view everyone's private directories.
-
-
@Application.get("/api/lock")
def GetLockEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
RequirePathAccessible(Root, Path, CurrentUser)
@@ -366,14 +341,9 @@ def DeleteLockEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurr
@Application.get("/api/locks")
def GetLocksEndpoint(CurrentUser: dict = Depends(RequireAdmin)) -> list[dict]:
- # The full list (across every root/path) stays admin-only — it's a
- # global overview, not a per-item action.
return LoadLocks()
-# --- Files ----------------------------------------------------------------
-
-
@Application.get("/api/roots")
def GetRootsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> list[dict]:
return ListRoots()
@@ -396,8 +366,6 @@ def GetTreeEndpoint(
Username = CurrentUser["Username"]
UserGroups = CurrentUser.get("Groups", [])
- # A locked subdirectory (or anything inside one) shouldn't even appear
- # in its parent's listing for someone without access to it.
return [
Entry
for Entry in Entries
@@ -412,9 +380,6 @@ def GetTreeEndpoint(
@Application.post("/api/note")
def PostNoteEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
- """Find (or create, for a directory that predates this feature) the
- directory's own note file, so it can be opened like any other file.
- """
RequirePathAccessible(Root, Path, CurrentUser)
try:
@@ -625,11 +590,6 @@ def GetBlameEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurren
def GetRawFileEndpoint(
RootName: str, FilePath: str, CurrentUser: dict = Depends(GetCurrentUser)
):
- """Serves a file at a URL that mirrors its real directory structure
- (unlike /api/file's query-param form), so relative links, stylesheets,
- and scripts INSIDE a previewed HTML document resolve correctly against
- sibling files in the same directory.
- """
RequirePathAccessible(RootName, FilePath, CurrentUser)
try:
@@ -663,9 +623,6 @@ def GetBookEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrent
return Book or {}
-# --- Settings / themes ------------------------------------------------------
-
-
@Application.get("/api/settings")
def GetSettingsEndpoint(CurrentUser: dict = Depends(GetCurrentUser)) -> dict:
return LoadSettings(CurrentUser["Username"])
@@ -719,10 +676,6 @@ def DeleteThemeEndpoint(Name: str, CurrentUser: dict = Depends(GetCurrentUser))
return {"Deleted": True}
-# In production, the frontend's built assets are copied alongside this file
-# under Frontend/dist (see Dockerfile) and served directly by the backend.
-# This stays unauthenticated — the SPA itself needs to load before it can
-# even show a login screen.
FrontendDist = ApplicationDirectory.parent / "Frontend" / "dist"
if FrontendDist.is_dir():
Application.mount("/", StaticFiles(directory=str(FrontendDist), html=True), name="Frontend")
diff --git a/Backend/Preview.py b/Backend/Preview.py
index 35093b5..4ed3ae9 100644
--- a/Backend/Preview.py
+++ b/Backend/Preview.py
@@ -20,7 +20,6 @@ MimeTypeMap = {
def ReadTextFile(FilePath: Path) -> str:
- """Read a text-classified file as UTF-8."""
return FilePath.read_text(encoding="utf-8")
@@ -33,7 +32,6 @@ def GetMimeType(FilePath: Path) -> str:
def ParseRangeHeader(RangeHeader: str, FileSize: int) -> tuple[int, int]:
- """Parse a single-range HTTP Range header into an inclusive (Start, End)."""
RangeValue = RangeHeader.replace("bytes=", "").strip()
StartText, _, EndText = RangeValue.partition("-")
diff --git a/Backend/SaraViewConf.toml b/Backend/SaraViewConf.toml
index 0d5cbdd..167095f 100644
--- a/Backend/SaraViewConf.toml
+++ b/Backend/SaraViewConf.toml
@@ -1,7 +1,3 @@
-# SaraView configuration.
-# Paths here are resolved as given — use absolute container paths when running
-# via Docker (see docker-compose.yml), matching whatever you mount the volumes to.
-
[SaraView]
ConfigVersion = 1
ApplicationVersion = "0.1.0-beta"
@@ -10,22 +6,12 @@ ApplicationVersion = "0.1.0-beta"
Host = "0.0.0.0"
Port = 8420
-# Where SaraView keeps its own data — Settings.toml and the Themes/ folder.
-# This is NOT one of the [Roots] below, so it is structurally unreachable
-# from the file browser regardless of any hidden-file setting. Give it its
-# own persistent volume (see docker-compose.yml) so it survives rebuilds.
[Storage]
DataDirectory = "/data/SaraViewData"
-# Named roots the browser can navigate. Add as many as you like; each becomes
-# a top-level entry point in the frontend's root switcher.
[Roots]
Notes = "/data/notes"
-# Integration with SaraTag: if Enabled, SaraView imports SaraTag's own Core.py
-# directly (by file path, not by installing it as a package) to resolve Book
-# tag files exactly the way SaraTag itself does. This is read-only — SaraView
-# never writes Book or Lexicon files.
[SaraTag]
Enabled = true
CoreModulePath = "/data/SaraTag/.saratag"
diff --git a/Backend/Settings.py b/Backend/Settings.py
index 4856d2f..a68d9e3 100644
--- a/Backend/Settings.py
+++ b/Backend/Settings.py
@@ -51,7 +51,6 @@ DefaultThemesDirectory = ApplicationDirectory / "DefaultThemes"
def SanitizeThemeName(ThemeName: str) -> str:
- """Reject theme names that could escape the Themes directory."""
if not ThemeName or "/" in ThemeName or "\\" in ThemeName or ThemeName in (".", ".."):
raise ValueError(f"Invalid theme name: {ThemeName}")
@@ -59,9 +58,6 @@ def SanitizeThemeName(ThemeName: str) -> str:
def SanitizeUsernameForPath(Username: str) -> str:
- """Usernames are validated at account-creation time, but this is a second
- line of defense against a settings path ever escaping its directory.
- """
if not Username or not re.fullmatch(r"[A-Za-z0-9_.-]+", Username):
raise ValueError(f"Invalid username: {Username}")
@@ -88,7 +84,6 @@ def GetThemesDirectory() -> Path:
def EnsureDataDirectory() -> None:
- """Create the data directory and seed default themes, if missing."""
DataDirectory = GetDataDirectory()
ThemesDirectory = GetThemesDirectory()
DataDirectory.mkdir(parents=True, exist_ok=True)
@@ -103,10 +98,6 @@ def EnsureDataDirectory() -> None:
def LoadSettings(Username: str) -> dict[str, Any]:
- """Load one user's settings, seeding their file with the defaults on
- first use. Settings are per-user; there is no shared/global settings
- file anymore.
- """
EnsureDataDirectory()
SettingsPath = GetSettingsPath(Username)
@@ -176,10 +167,6 @@ def LoadTheme(ThemeName: str) -> dict[str, str]:
def SaveTheme(ThemeName: str, Palette: dict[str, str]) -> None:
- """Create or overwrite a custom theme. Missing roles fall back to the
- active Catppuccin Mocha default so a partially-specified custom theme
- still renders sensibly.
- """
EnsureDataDirectory()
ThemeName = SanitizeThemeName(ThemeName)
diff --git a/Backend/Users.py b/Backend/Users.py
index af427e3..e55598a 100644
--- a/Backend/Users.py
+++ b/Backend/Users.py
@@ -193,7 +193,6 @@ def SaveAvatar(Username: str, FileBytes: bytes, Extension: str) -> None:
Directory = GetAvatarDirectory()
Directory.mkdir(parents=True, exist_ok=True)
- # Remove any previous avatar for this user under a different extension.
for ExistingPath in Directory.glob(f"{Username}.*"):
ExistingPath.unlink()
diff --git a/Frontend/src/Api.js b/Frontend/src/Api.js
index d5e62da..d14fead 100644
--- a/Frontend/src/Api.js
+++ b/Frontend/src/Api.js
@@ -27,8 +27,6 @@ function JsonBody(Body) {
return { headers: { "Content-Type": "application/json" }, body: JSON.stringify(Body) };
}
-// --- Auth -------------------------------------------------------------
-
export function FetchAuthStatus() {
return FetchJson("/auth/status");
}
@@ -92,8 +90,6 @@ export function ChangePassword(CurrentPassword, NewPassword) {
});
}
-// --- Groups ---------------------------------------------------------------
-
export function FetchGroups() {
return FetchJson("/groups");
}
@@ -112,8 +108,6 @@ export function SetGroupMembersApi(Name, Members) {
return FetchJson(`/groups/members?${Params}`, { method: "PUT", ...JsonBody({ Members }) });
}
-// --- Locked files/directories -----------------------------------------
-
export function FetchLocks() {
return FetchJson("/locks");
}
@@ -133,8 +127,6 @@ export function RemoveLock(RootName, RelativePath) {
return FetchJson(`/lock?${Params}`, { method: "DELETE" });
}
-// --- Files --------------------------------------------------------------
-
export function FetchRoots() {
return FetchJson("/roots");
}
@@ -199,8 +191,6 @@ export function EnsureDirectoryNote(RootName, RelativePath) {
return FetchJson(`/note?${Params}`, { method: "POST" });
}
-// --- Settings / themes -------------------------------------------------
-
export function FetchSettings() {
return FetchJson("/settings");
}
diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx
index ce15426..8ae53a6 100644
--- a/Frontend/src/App.jsx
+++ b/Frontend/src/App.jsx
@@ -40,7 +40,7 @@ function GetParentPath(FilePath) {
}
export default function App() {
- const [AuthState, SetAuthState] = useState("Loading"); // Loading | Bootstrap | LoggedOut | LoggedIn
+ const [AuthState, SetAuthState] = useState("Loading");
const [CurrentUser, SetCurrentUser] = useState(null);
const [Roots, SetRoots] = useState([]);
@@ -128,9 +128,6 @@ export default function App() {
SetCurrentDirectoryPath(DirectoryPath);
}
- // A directory is opened the same way a file is - via its own note file -
- // rather than by drilling into its listing. Browsing a directory's
- // contents happens separately, through the sidebar tree.
function HandleSelectDirectoryNote(DirectoryPath, NoteName, NoteFileType) {
SetViewPath(DirectoryPath);
SetViewIsDirectory(false);
@@ -169,8 +166,6 @@ export default function App() {
RefreshSidebar();
if (ViewNoteFilePath && ViewPath === OldPath) {
- // Viewing this directory's own note - the server already renamed the
- // note file to match, so just re-resolve it under the new path.
OpenDirectoryNote(NewPath, null);
} else if (!ViewIsDirectory && ViewPath === OldPath) {
HandleSelectFile(NewPath, ViewFileType);
@@ -203,8 +198,6 @@ export default function App() {
return