Compare commits

..

4 Commits

Author SHA1 Message Date
Arunavo Ray
832b57538d chore: bump version to 2.11.0 2025-05-28 14:08:45 +05:30
Arunavo Ray
415bff8e41 feat: enhance Organizations page with live refresh and fix repository breakdown bug
- Add live refresh functionality to Organizations page using the same pattern as Repositories and Activity Log pages
- Fix repository breakdown bug where public/private/fork counts disappeared after toggling mirroring
- Change toggle text from 'Include in mirroring' to 'Enable mirroring' for better clarity
- Automatically refresh organization data after mirroring starts to maintain breakdown visibility
- Clean up unused imports and variables for better code quality
2025-05-28 14:08:07 +05:30
Arunavo Ray
13c3ddea04 Added a small gap to Verison Info 2025-05-28 13:55:50 +05:30
Arunavo Ray
b917b30830 docs: add Docker bind mount vs named volume permission guidance
- Add new section 'Docker Volume Types and Permissions'
- Explain difference between named volumes and bind mounts
- Provide solution for bind mount permission issues (UID 1001)
- Clarify why named volumes are recommended and used in official docker-compose.yml
- Address SQLite permission errors in Docker environments using bind mounts

Addresses issue reported by user using bind mounts in Portainer.
2025-05-28 13:37:07 +05:30
5 changed files with 77 additions and 20 deletions

View File

@@ -513,6 +513,36 @@ Try the following steps:
> >
> This setup provides a complete containerized deployment for the Gitea Mirror application. > This setup provides a complete containerized deployment for the Gitea Mirror application.
#### Docker Volume Types and Permissions
> [!IMPORTANT]
> **Named Volumes vs Bind Mounts**: If you encounter SQLite permission errors even when using Docker, check your volume configuration:
**✅ Named Volumes (Recommended):**
```yaml
volumes:
- gitea-mirror-data:/app/data # Docker manages permissions automatically
```
**⚠️ Bind Mounts (Requires Manual Permission Setup):**
```yaml
volumes:
- /host/path/to/data:/app/data # Host filesystem permissions apply
```
**If using bind mounts**, ensure the host directory is owned by UID 1001 (the `gitea-mirror` user):
```bash
# Set correct ownership for bind mount
sudo chown -R 1001:1001 /host/path/to/data
sudo chmod -R 755 /host/path/to/data
```
**Why named volumes work better:**
- Docker automatically handles permissions
- Better portability across different hosts
- No manual permission setup required
- Used by our official docker-compose.yml
#### Database Maintenance #### Database Maintenance

View File

@@ -1,7 +1,7 @@
{ {
"name": "gitea-mirror", "name": "gitea-mirror",
"type": "module", "type": "module",
"version": "2.10.0", "version": "2.11.0",
"engines": { "engines": {
"bun": ">=1.2.9" "bun": ">=1.2.9"
}, },

View File

@@ -37,7 +37,7 @@ export function VersionInfo() {
return ( return (
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2"> <div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
{versionInfo.updateAvailable ? ( {versionInfo.updateAvailable ? (
<div className="flex flex-col"> <div className="flex flex-col gap-1">
<span>v{versionInfo.current}</span> <span>v{versionInfo.current}</span>
<span className="text-primary">v{versionInfo.latest} available</span> <span className="text-primary">v{versionInfo.latest} available</span>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Search, RefreshCw, FlipHorizontal, Plus } from "lucide-react"; import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
import type { MirrorJob, Organization } from "@/lib/db/schema"; import type { MirrorJob, Organization } from "@/lib/db/schema";
import { OrganizationList } from "./OrganizationsList"; import { OrganizationList } from "./OrganizationsList";
import AddOrganizationDialog from "./AddOrganizationDialog"; import AddOrganizationDialog from "./AddOrganizationDialog";
@@ -26,6 +26,7 @@ import { useFilterParams } from "@/hooks/useFilterParams";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfigStatus } from "@/hooks/useConfigStatus"; import { useConfigStatus } from "@/hooks/useConfigStatus";
import { useNavigation } from "@/components/layout/MainLayout"; import { useNavigation } from "@/components/layout/MainLayout";
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
export function Organization() { export function Organization() {
const [organizations, setOrganizations] = useState<Organization[]>([]); const [organizations, setOrganizations] = useState<Organization[]>([]);
@@ -34,6 +35,7 @@ export function Organization() {
const { user } = useAuth(); const { user } = useAuth();
const { isGitHubConfigured } = useConfigStatus(); const { isGitHubConfigured } = useConfigStatus();
const { navigationKey } = useNavigation(); const { navigationKey } = useNavigation();
const { registerRefreshCallback } = useLiveRefresh();
const { filter, setFilter } = useFilterParams({ const { filter, setFilter } = useFilterParams({
searchTerm: "", searchTerm: "",
membershipRole: "", membershipRole: "",
@@ -62,19 +64,23 @@ export function Organization() {
onMessage: handleNewMessage, onMessage: handleNewMessage,
}); });
const fetchOrganizations = useCallback(async () => { const fetchOrganizations = useCallback(async (isLiveRefresh = false) => {
if (!user?.id) { if (!user?.id) {
return false; return false;
} }
// Don't fetch organizations if GitHub is not configured // Don't fetch organizations if GitHub is not configured
if (!isGitHubConfigured) { if (!isGitHubConfigured) {
setIsLoading(false); if (!isLiveRefresh) {
setIsLoading(false);
}
return false; return false;
} }
try { try {
setIsLoading(true); if (!isLiveRefresh) {
setIsLoading(true);
}
const response = await apiRequest<OrganizationsApiResponse>( const response = await apiRequest<OrganizationsApiResponse>(
`/github/organizations?userId=${user.id}`, `/github/organizations?userId=${user.id}`,
@@ -87,27 +93,47 @@ export function Organization() {
setOrganizations(response.organizations); setOrganizations(response.organizations);
return true; return true;
} else { } else {
toast.error(response.error || "Error fetching organizations"); if (!isLiveRefresh) {
toast.error(response.error || "Error fetching organizations");
}
return false; return false;
} }
} catch (error) { } catch (error) {
toast.error( if (!isLiveRefresh) {
error instanceof Error ? error.message : "Error fetching organizations" toast.error(
); error instanceof Error ? error.message : "Error fetching organizations"
);
}
return false; return false;
} finally { } finally {
setIsLoading(false); if (!isLiveRefresh) {
setIsLoading(false);
}
} }
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object }, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
useEffect(() => { useEffect(() => {
// Reset loading state when component becomes active // Reset loading state when component becomes active
setIsLoading(true); setIsLoading(true);
fetchOrganizations(); fetchOrganizations(false); // Manual refresh, not live
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation }, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
// Register with global live refresh system
useEffect(() => {
// Only register for live refresh if GitHub is configured
if (!isGitHubConfigured) {
return;
}
const unregister = registerRefreshCallback(() => {
fetchOrganizations(true); // Live refresh
});
return unregister;
}, [registerRefreshCallback, fetchOrganizations, isGitHubConfigured]);
const handleRefresh = async () => { const handleRefresh = async () => {
const success = await fetchOrganizations(); const success = await fetchOrganizations(false);
if (success) { if (success) {
toast.success("Organizations refreshed successfully."); toast.success("Organizations refreshed successfully.");
} }
@@ -140,6 +166,12 @@ export function Organization() {
return updated ? updated : org; return updated ? updated : org;
}) })
); );
// Refresh organization data to get updated repository breakdown
// Use a small delay to allow the backend to process the mirroring request
setTimeout(() => {
fetchOrganizations(true);
}, 1000);
} else { } else {
toast.error(response.error || "Error starting mirror job"); toast.error(response.error || "Error starting mirror job");
} }
@@ -258,12 +290,7 @@ export function Organization() {
} }
}; };
// Get unique organization names for combobox (since Organization has no owner field)
const ownerOptions = Array.from(
new Set(
organizations.map((org) => org.name).filter((v): v is string => !!v)
)
).sort();
return ( return (
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">

View File

@@ -172,7 +172,7 @@ export function OrganizationList({
htmlFor={`include-${org.id}`} htmlFor={`include-${org.id}`}
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Include in mirroring Enable mirroring
</label> </label>
{isLoading && ( {isLoading && (