Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<a href="https://nixopus.com"><b>Website</b></a> •
<a href="https://docs.nixopus.com"><b>Documentation</b></a> •
<a href="https://docs.nixopus.com/blog/"><b>Blog</b></a> •
<a href="https://discord.gg/skdcq39Wpv"><b>Discord</b></a>
<a href="https://discord.gg/skdcq39Wpv"><b>Discord</b></a> •
<a href="https://github.com/raghavyuva/nixopus/discussions/262"><b>Roadmap</b></a>
</p>

<img width="1210" height="764" alt="image" src="https://github.com/user-attachments/assets/3f1dc1e0-956d-4785-8745-ed59d0390afd" />
Expand Down Expand Up @@ -80,3 +81,4 @@ Nixopus is derived from the combination of "octopus" and the Linux penguin (Tux)
<a href="https://github.com/raghavyuva/nixopus/graphs/contributors">
<img src="https://contrib.rocks/image?repo=raghavyuva/nixopus" alt="Nixopus project contributors" />
</a>

2 changes: 1 addition & 1 deletion api/api/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"version": "v1",
"status": "active",
"release_date": "2025-09-11T20:17:18.854289411+05:30",
"release_date": "2025-09-18T09:02:53.080017+05:30",
"end_of_life": "0001-01-01T00:00:00Z",
"changes": [
"Initial API version"
Expand Down
5 changes: 4 additions & 1 deletion api/internal/features/deploy/tasks/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/google/uuid"
"github.com/raghavyuva/caddygo"
"github.com/raghavyuva/nixopus-api/internal/config"
"github.com/raghavyuva/nixopus-api/internal/features/deploy/types"
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
)
Expand Down Expand Up @@ -81,7 +82,9 @@ func (t *TaskService) HandleCreateDockerfileDeployment(ctx context.Context, Task
taskCtx.LogAndUpdateStatus("Failed to convert port to int: "+err.Error(), shared_types.Failed)
return err
}
err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, TaskPayload.Application.Domain, port, caddygo.DomainOptions{})
upstreamHost := config.AppConfig.SSH.Host

err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, upstreamHost, port, caddygo.DomainOptions{})
if err != nil {
Comment on lines +85 to 88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add guard + fallback to avoid proxy loop and empty upstream; handle reload error

If config.AppConfig.SSH.Host is empty or equals the app domain, the Caddy proxy loop can reappear or config will be invalid. Also, client.Reload() errors are ignored.

Apply this diff:

   port, err := strconv.Atoi(containerResult.AvailablePort)
   if err != nil {
     taskCtx.LogAndUpdateStatus("Failed to convert port to int: "+err.Error(), shared_types.Failed)
     return err
   }
-  upstreamHost := config.AppConfig.SSH.Host
+  upstreamHost := config.AppConfig.SSH.Host
+  if upstreamHost == "" || upstreamHost == TaskPayload.Application.Domain {
+    taskCtx.AddLog("Upstream host unset or equals domain; defaulting to 127.0.0.1")
+    upstreamHost = "127.0.0.1"
+  }
 
-  err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, upstreamHost, port, caddygo.DomainOptions{})
+  err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, upstreamHost, port, caddygo.DomainOptions{})
   if err != nil {
     fmt.Println("Failed to add domain: ", err)
     taskCtx.LogAndUpdateStatus("Failed to add domain: "+err.Error(), shared_types.Failed)
     return err
   }
-  client.Reload()
+  if err := client.Reload(); err != nil {
+    taskCtx.LogAndUpdateStatus("Failed to reload Caddy: "+err.Error(), shared_types.Failed)
+    return err
+  }
   return nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
upstreamHost := config.AppConfig.SSH.Host
err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, upstreamHost, port, caddygo.DomainOptions{})
if err != nil {
port, err := strconv.Atoi(containerResult.AvailablePort)
if err != nil {
taskCtx.LogAndUpdateStatus("Failed to convert port to int: "+err.Error(), shared_types.Failed)
return err
}
upstreamHost := config.AppConfig.SSH.Host
if upstreamHost == "" || upstreamHost == TaskPayload.Application.Domain {
taskCtx.AddLog("Upstream host unset or equals domain; defaulting to 127.0.0.1")
upstreamHost = "127.0.0.1"
}
err = client.AddDomainWithAutoTLS(TaskPayload.Application.Domain, upstreamHost, port, caddygo.DomainOptions{})
if err != nil {
fmt.Println("Failed to add domain: ", err)
taskCtx.LogAndUpdateStatus("Failed to add domain: "+err.Error(), shared_types.Failed)
return err
}
if err := client.Reload(); err != nil {
taskCtx.LogAndUpdateStatus("Failed to reload Caddy: "+err.Error(), shared_types.Failed)
return err
}
return nil
🤖 Prompt for AI Agents
In api/internal/features/deploy/tasks/create.go around lines 85-88, add a guard
so upstreamHost is not empty and does not equal the app domain: if
config.AppConfig.SSH.Host == "" || config.AppConfig.SSH.Host ==
TaskPayload.Application.Domain then set upstreamHost to a safe fallback like
"127.0.0.1" (so the Caddy upstream is valid and avoids proxy loops) before
calling AddDomainWithAutoTLS; also ensure you check and handle the error
returned by client.Reload() (return or log the error) instead of ignoring it so
reload failures are surfaced.

fmt.Println("Failed to add domain: ", err)
taskCtx.LogAndUpdateStatus("Failed to add domain: "+err.Error(), shared_types.Failed)
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ services:

nixopus-redis:
image: redis:7-alpine
container_name: nixopus-redis-container
container_name: nixopus-redis
restart: unless-stopped
ports:
- "${REDIS_PORT:-6379}:6379"
Expand Down
1 change: 1 addition & 0 deletions helpers/config.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
SSH_PASSWORD: ${SSH_PASSWORD:-}
DOCKER_HOST: ${DOCKER_HOST:-unix:///var/run/docker.sock}
REDIS_URL: ${REDIS_URL:-redis://localhost:6379}
CADDY_ENDPOINT: ${CADDY_ENDPOINT:-http://127.0.0.1:2019}
ALLOWED_ORIGIN: ${ALLOWED_ORIGIN:-http://localhost:7443}
ENV: ${ENV:-development}
LOGS_PATH: ${LOGS_PATH:-./logs}
Expand Down
8 changes: 4 additions & 4 deletions view/app/containers/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ const ContainersTable = ({

const hasPorts = container.ports && container.ports.length > 0;
const formattedDate = container.created
? new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'long' }).format(
new Date(parseInt(container.created) * 1000)
)
: '-';
? new Intl.DateTimeFormat('en-US', { month: 'short', day: '2-digit', year: 'numeric' }).format(
new Date(parseInt(container.created) * 1000)
)
: '-';

return (
<TableRow key={container.id} onClick={() => router.push(`/containers/${container.id}`)}
Expand Down
2 changes: 2 additions & 0 deletions view/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,5 @@
scrollbar-width: none;
}
}


2 changes: 1 addition & 1 deletion view/components/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function AppSidebar({
}

return (
<Sidebar collapsible="icon" {...props}>
<Sidebar className="sidebar" collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher refetch={refetch} />
</SidebarHeader>
Expand Down
18 changes: 14 additions & 4 deletions view/components/layout/nav-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,6 @@ Add any other context about the problem here.`;
<HelpCircle />
{t('user.menu.help')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleReportIssue}>
<AlertCircle />
{t('user.menu.reportIssue')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut />
Expand All @@ -232,6 +228,20 @@ Add any other context about the problem here.`;
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>

{/* Bottom-aligned actions */}
<SidebarMenuItem className="mt-auto" onClick={handleSponsor}>
<Heart className="text-red-500" />
{t('user.menu.sponsor')}
</SidebarMenuItem>
<SidebarMenuItem onClick={handleHelp}>
<HelpCircle />
{t('user.menu.help')}
</SidebarMenuItem>
<SidebarMenuItem onClick={handleReportIssue}>
<AlertCircle />
{t('user.menu.reportIssue')}
</SidebarMenuItem>
</SidebarMenu>
Comment on lines +233 to 245
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

A11y: use button components for interactive list items.

Clickable

  • harms keyboard/focus/semantics. Wrap with SidebarMenuButton.

    Apply:

    -      <SidebarMenuItem className="mt-auto" onClick={handleSponsor}>
    -        <Heart className="text-red-500" />
    -        {t('user.menu.sponsor')}
    -      </SidebarMenuItem>
    -      <SidebarMenuItem onClick={handleHelp}>
    -        <HelpCircle />
    -        {t('user.menu.help')}
    -      </SidebarMenuItem>
    -      <SidebarMenuItem onClick={handleReportIssue}>
    -        <AlertCircle />
    -        {t('user.menu.reportIssue')}
    -      </SidebarMenuItem>
    +      <SidebarMenuItem className="mt-auto">
    +        <SidebarMenuButton onClick={handleSponsor}>
    +          <Heart className="text-red-500" />
    +          {t('user.menu.sponsor')}
    +        </SidebarMenuButton>
    +      </SidebarMenuItem>
    +      <SidebarMenuItem>
    +        <SidebarMenuButton onClick={handleHelp}>
    +          <HelpCircle />
    +          {t('user.menu.help')}
    +        </SidebarMenuButton>
    +      </SidebarMenuItem>
    +      <SidebarMenuItem>
    +        <SidebarMenuButton onClick={handleReportIssue}>
    +          <AlertCircle />
    +          {t('user.menu.reportIssue')}
    +        </SidebarMenuButton>
    +      </SidebarMenuItem>
    📝 Committable suggestion

    ‼️ IMPORTANT
    Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    Suggested change
    <SidebarMenuItem className="mt-auto" onClick={handleSponsor}>
    <Heart className="text-red-500" />
    {t('user.menu.sponsor')}
    </SidebarMenuItem>
    <SidebarMenuItem onClick={handleHelp}>
    <HelpCircle />
    {t('user.menu.help')}
    </SidebarMenuItem>
    <SidebarMenuItem onClick={handleReportIssue}>
    <AlertCircle />
    {t('user.menu.reportIssue')}
    </SidebarMenuItem>
    </SidebarMenu>
    <SidebarMenuItem className="mt-auto">
    <SidebarMenuButton onClick={handleSponsor}>
    <Heart className="text-red-500" />
    {t('user.menu.sponsor')}
    </SidebarMenuButton>
    </SidebarMenuItem>
    <SidebarMenuItem>
    <SidebarMenuButton onClick={handleHelp}>
    <HelpCircle />
    {t('user.menu.help')}
    </SidebarMenuButton>
    </SidebarMenuItem>
    <SidebarMenuItem>
    <SidebarMenuButton onClick={handleReportIssue}>
    <AlertCircle />
    {t('user.menu.reportIssue')}
    </SidebarMenuButton>
    </SidebarMenuItem>
    </SidebarMenu>
    🤖 Prompt for AI Agents
    In view/components/layout/nav-user.tsx around lines 233 to 245, the interactive
    SidebarMenuItem list items are implemented as clickable <li> elements which
    breaks keyboard/focus semantics; replace each interactive SidebarMenuItem with
    the provided SidebarMenuButton (or wrap the existing content in
    SidebarMenuButton) so the onClick, className and icons/translations move to the
    button element, preserving styling and handlers for handleSponsor, handleHelp,
    and handleReportIssue; ensure the button receives the mt-auto class where used
    and that tab/keyboard activation works as before.
    
  • );
    }
    6 changes: 3 additions & 3 deletions view/components/ui/password-input-field.tsx
    Original file line number Diff line number Diff line change
    Expand Up @@ -9,17 +9,17 @@ export interface PasswordInputFieldProps extends React.ComponentProps<'input'> {
    }

    const PasswordInputField = React.forwardRef<HTMLInputElement, PasswordInputFieldProps>(
    function PasswordInputField({ className, containerClassName, ...props }, ref) {
    function PasswordInputField({ className, containerClassName, autoComplete, ...props }, ref) {
    const [showPassword, setShowPassword] = React.useState(false);

    return (
    <div className={cn('relative', containerClassName)}>
    <Input
    ref={ref}
    {...props}
    type={showPassword ? 'text' : 'password'}
    className={cn('pr-10', className)}
    autoComplete={props.autoComplete ?? 'current-password'}
    {...props}
    autoComplete={autoComplete ?? 'current-password'}
    />
    <button
    type="button"
    Expand Down
    3 changes: 2 additions & 1 deletion view/components/ui/sidebar.tsx
    Original file line number Diff line number Diff line change
    Expand Up @@ -431,12 +431,13 @@ function SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'
    );
    }

    // Ensure the sidebar supports bottom anchoring
    function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
    return (
    <ul
    data-slot="sidebar-menu"
    data-sidebar="menu"
    className={cn('flex w-full min-w-0 flex-col gap-1', className)}
    className={cn('flex w-full min-w-0 flex-col gap-1', className)} // Already supports flex-column
    {...props}
    />
    );
    Expand Down