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 ; } - // Only take up a TopBar row at all if there's something to put in it — - // otherwise turning breadcrumbs off with a single root left a blank strip. const ShowTopBar = (Settings && Settings.ShowBreadcrumbs) || Roots.length > 1; const ConfirmBeforeDelete = Settings ? Settings.ConfirmBeforeDelete : true; diff --git a/Frontend/src/Components/LoginScreen.jsx b/Frontend/src/Components/LoginScreen.jsx index 57197fe..75538f3 100644 --- a/Frontend/src/Components/LoginScreen.jsx +++ b/Frontend/src/Components/LoginScreen.jsx @@ -40,7 +40,7 @@ export default function LoginScreen({ IsBootstrap, OnAuthenticated }) { {IsBootstrap && (
- No accounts exist yet — this first one becomes an admin. + No accounts exist yet - this first one becomes an admin.
)}
diff --git a/Frontend/src/Components/PreviewPane.jsx b/Frontend/src/Components/PreviewPane.jsx index 88e1f8a..f9f98d7 100644 --- a/Frontend/src/Components/PreviewPane.jsx +++ b/Frontend/src/Components/PreviewPane.jsx @@ -141,7 +141,7 @@ function BuildBlameGutter(BlameMap, ShowAvatars) { return new BlameMarker("hidden", "•••", null, Title); } - const Title = `${Entry.Username} — ${new Date(Entry.Timestamp).toLocaleString()}`; + const Title = `${Entry.Username} - ${new Date(Entry.Timestamp).toLocaleString()}`; if (ShowAvatars && Entry.HasAvatar) { return new BlameMarker("avatar", GetAvatarUrl(Entry.Username), null, Title); @@ -242,9 +242,6 @@ export default function PreviewPane({
- {/* Raw, path-shaped URL (not the query-param /api/file form) so - relative /