Skip to content

Comments

feat: implement landing page and sidebar UI#22

Closed
Rl0007 wants to merge 29 commits intodevelopfrom
ui-changes
Closed

feat: implement landing page and sidebar UI#22
Rl0007 wants to merge 29 commits intodevelopfrom
ui-changes

Conversation

@Rl0007
Copy link
Contributor

@Rl0007 Rl0007 commented Dec 14, 2025

Summary by CodeRabbit

  • New Features

    • Public landing site/layout, interactive sidebar, hero & features; App Version list and DocType
    • Redesigned Apps UI (Frappe vs Custom), custom app form with branch support, register/delete custom app APIs
    • Custom image preview/build/enqueue, build status endpoint, image tagging and rebuild flow
    • Theme design tokens, input mask plugin, blink animation utility
  • Bug Fixes

    • Home page routing corrected; success toast color fixed
  • Chores

    • CI runtimes updated; deployment playbook now performs a fresh repo clone

✏️ Tip: You can customize this high-level summary in your review settings.

Add public landing page and PenguinUI sidebar navigation.
Still needs browser cache clearing and layout fixes.

Partial implementation of #19
@Venkateshvenki404224 Venkateshvenki404224 marked this pull request as draft December 25, 2025 11:46
@coderabbitai
Copy link

coderabbitai bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

Adds public site/layout and landing pages, new App Version DocType, CI runtime updates, UI and layout refactors (sidebar, mask plugin, theme tokens), Custom Image and Apps privacy/builds refactor with new APIs for registering/deleting custom apps, Ansible clone replaced with fresh checkout, and i18n of tooling errors.

Changes

Cohort / File(s) Summary
CI & Hooks
/.github/workflows/ci.yml, nano_press/hooks.py
CI: Python 3.10 → 3.14, Node 18 → 24. home_page changed "login""index"; scheduler_events commented out; fixture entry added ({"dt":"App Version"}).
Apps DocType & model
nano_press/nano_press/doctype/apps/apps.json, nano_press/nano_press/doctype/apps/apps.py
DocType: added app_logo, column_break_eisq, is_custom, set image_field: "app_logo". Model: added validate() to set is_public based on is_custom/pat_token.
Custom Image DocType & logic
nano_press/nano_press/doctype/custom_image/custom_image.json, .../custom_image/custom_image.py
frappe_version field changed from Select → Link to App Version. Large refactor: many private helpers for apps JSON/base64, tag sanitization, PAT injection, sorting, centralized status/notification flow; added enqueue/preview APIs and build lifecycle handlers; updated public API surface.
App Version DocType & model
nano_press/nano_press/doctype/app_version/*, nano_press/fixtures/app_version.json
New DocType "App Version" with version and scrubbed_version; model adds autoname(); fixtures added for versions.
Ansible playbook
nano_press/nano_press/utils/ansible/playbooks/build_custom_image.yml
Replace in-place repo update with remove-then-fresh-clone of frappe_docker to ensure clean checkout.
I18n in tooling
nano_press/nano_press/utils/ansible/src/AnsibleRunner.py, nano_press/nano_press/utils/remote_builder.py, nano_press/utils/ansible_runner.py
Import frappe._ and wrap user-facing error strings with _(); no control-flow changes.
Public layout & landing
nano_press/templates/layout_public.html, nano_press/www/index.html, nano_press/www/index.py, nano_press/templates/components/landing/*
New public layout, index page, and landing (hero + features); no_cache=1 for index; head/meta handling and asset inclusion added.
Main layout & sidebar
nano_press/templates/layout.html, nano_press/templates/includes/sidebar.html
Body restructured to include new Alpine-driven responsive sidebar, inner scroll container, toast/footer includes, and mask plugin injection.
Client mask plugin
nano_press/public/js/vendor/alpinejs/plugins/mask.js
New Alpine mask directive plugin for input masking, model sync, cursor preservation, and teardown.
Theme tokens & styles
nano_press/public/css/theme.css, nano_press/public/css/main.css
Added Penguin UI design tokens (color/surface/text/outline, --radius) and .animate-blink utility with keyframes and reduced-motion support.
Apps UI & custom-app flow
nano_press/templates/components/core/apps.html, nano_press/templates/includes/home/custom_app_modal.html, nano_press/public/js/script.js
UI split into frappeApps/customApps; modal reworked with Public/Private tabs and branch field; client state adds appType and customAppForm.branch; addCustomApp/removeCustomApp call new backend endpoints and sync store.
Server selection & installer UI
nano_press/templates/components/core/server.html, nano_press/templates/components/core/summary.html, nano_press/templates/components/core/stepper.html
Add "Select Existing Server" dropdown populated from get_user_servers(); selection populates server fields and verification state; summary/stepper layout tweaks (z-index change).
Domain verification flow
nano_press/templates/components/core/domain.html
Copy updated to "Add Domain"; DNS instruction block added; verification now always attempted when domain provided; next-step logic adjusted.
Public site data endpoints
nano_press/www/self_host.py, nano_press/www/self-host.html
Added get_user_servers() and get_frappe_versions() helpers; get_apps() exposes frappe and app_logo; store default normalized to version-15.
API for custom apps
nano_press/api.py
Added register_custom_app(...) and delete_custom_app(app_name) (frappe.whitelist) with validation, create/update/delete logic, logging and transaction handling.
Frappe Site & server logic
nano_press/nano_press/doctype/frappe_site/*, nano_press/nano_press/doctype/server/server.py
FrappeSite: custom-image status sync, pre-deploy guards, rebuild_custom_image_for_site endpoint; frappe_site.json status options expanded (e.g., Building, Build Failed); server error messages wrapped in _().
Templates & small UI tweaks
nano_press/templates/includes/toast-teleport.html, various templates
Minor template adjustments (toast success color, layout includes, structure updates, new footer include).

Sequence Diagram(s)

sequenceDiagram
    participant User as Browser (User)
    participant Modal as Custom App Modal (UI)
    participant Client as Client Store/Validator
    participant Backend as Server API
    participant Apps as Apps DocType (DB)

    User->>Modal: Open modal, choose Public/Private, fill name, githubUrl, branch, token?
    Modal->>Client: Validate inputs
    alt Valid
        Client->>Backend: register_custom_app(name, githubUrl, branch, token?, order?)
        Backend->>Apps: create or update Apps record (is_custom=1)
        Apps-->>Backend: saved
        Backend-->>Client: success {app_reference_id, message}
        Client->>Modal: reset form, close modal
        Modal->>User: show success toast
    else Invalid
        Client-->>Modal: return validation errors
        Modal->>User: show errors
    end
Loading
sequenceDiagram
    participant User as Browser (User)
    participant DomainComp as Domain Component (UI)
    participant Verifier as Client Verifier
    participant DNS as DNS/Resolver
    participant Store as Local Store

    User->>DomainComp: Enter domain, click Next
    DomainComp->>Store: clear domainVerified
    DomainComp->>Verifier: trigger verification (always if domain provided)
    Verifier->>DNS: resolve / check A record
    alt DNS verifies
        DNS-->>Verifier: verified
        Verifier->>Store: set domainVerified = true
        Verifier->>DomainComp: advance to next step
    else Not verified
        DNS-->>Verifier: not found/error
        Verifier->>Store: set domainVerified = false
        Verifier->>DomainComp: show error toast
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 A rabbit's hop for the PR:
New pages bloom and sidebars sing,
Custom apps get lock and spring.
Domains prove their rightful claim,
Fresh clones make clean builds the game.
Hooray — tiny hops, big change!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: implementation of landing page UI components (hero, features sections) and sidebar component with interactive features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb91851 and c1ad4e8.

📒 Files selected for processing (2)
  • nano_press/fixtures/app_version.json
  • nano_press/hooks.py
✅ Files skipped from review due to trivial changes (1)
  • nano_press/fixtures/app_version.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • nano_press/hooks.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Venkateshvenki404224
Copy link
Collaborator

image

Update the sidebar to include the profile in the bottom and also increased the width of the center container

- Fix success toast notification color from red to green
- Replace text "Remove" button with Heroicons trash icon in red
…he app is public or private, based on this toggle if the github token is required or not.
@Rl0007
Copy link
Contributor Author

Rl0007 commented Dec 26, 2025

@Venkateshvenki404224 the linters are failing

@Venkateshvenki404224
Copy link
Collaborator

@Venkateshvenki404224 the linters are failing

I am on it

- Split apps into two distinct sections with headers
- Frappe Apps section displays core apps (frappe === 1) with green gradient selection
- Custom Apps section shows database custom apps and user-added apps
- Updated backend to fetch 'frappe' field from Apps doctype
- Added computed properties to filter apps by type
- Implemented inline error validation for custom app form
- Fixed domain verification to always re-verify on each visit
@Rl0007 Rl0007 marked this pull request as ready for review December 27, 2025 05:39
Comment on lines +154 to +160
# scheduler_events = {
# "cron": {
# "*/5 * * * *": [
# "nano_press.utils.ansible_runner.ping_server",
# ],
# }
# }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why are we removing this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Because the ping server function expects ip address or server doc so I didn't have any idea on how to pass argument from hooks.py

});
this.customAppForm = { name: '', githubUrl: '', token: '' };
this.showCustomAppDialog = false;
// Clear all errors
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this comment needed?

Comment on lines 73 to 99
this.formErrors = { name: '', githubUrl: '', token: '', branch: '' };

let hasError = false;

// Validate all required fields
if (!this.customAppForm.name) {
this.formErrors.name = 'App name is required.';
hasError = true;
}

if (!this.customAppForm.githubUrl) {
this.formErrors.githubUrl = 'GitHub URL is required.';
hasError = true;
} else if (!this.customAppForm.githubUrl.startsWith('https://')) {
this.formErrors.githubUrl = 'GitHub URL must start with https://';
hasError = true;
}

if (this.appType === 'private' && !this.customAppForm.token) {
this.formErrors.token =
'GitHub Token is required for private repositories.';
hasError = true;
}

if (!this.customAppForm.branch) {
this.formErrors.branch = 'Branch is required.';
hasError = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of manually checking can we use the HTML validation like this https://github.com/BuildWithHussain/ls_shop/blob/develop/ls_shop/www/account/profile.html

On form submit we collect the data and add additional validations, everything else is handled by HTML form validations

<!-- SECTION 2: Custom Apps -->
<h3 class="text-sm font-semibold mb-3 mt-6 text-gray-900">Custom Apps</h3>

<!-- Database custom apps (frappe === 0) -->
Copy link
Contributor Author

@Rl0007 Rl0007 Dec 27, 2025

Choose a reason for hiding this comment

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

Can we remove this comment?

</div>
</template>

<!-- "Add Custom App" Button -->
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's remove this and other comments

{% set full_name = frappe.utils.get_fullname(frappe.session.user) if frappe.utils.get_fullname else frappe.session.user %}

<div x-data="{ showSidebar: false }" class="relative flex w-full flex-col md:flex-row">
<!-- This allows screen readers to skip the sidebar and go directly to the main content. -->
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's remove this comments

{% include "templates/includes/sidebar.html" %}

<div class="overflow-y-auto" x-data="toaster">
<div class="w-full px-6 md:px-10 py-8">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's cap the width at max-w-7xl.
Reason being on extremely large devices it takes complete width
Screenshot 2025-12-27 at 11 04 51 AM

It's better to have it contained.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
nano_press/nano_press/doctype/custom_image/custom_image.py (1)

90-97: PAT token is exposed via API endpoint and should not be included in responses sent to the frontend.

The _build_repo_url() function correctly embeds the PAT token into the URL for private repository access. However, on line 54, this entire apps configuration (including the token-embedded URL) is serialized to JSON via json.dumps(sorted_apps, indent=2). This JSON is then returned by the @frappe.whitelist() method preview_apps_json_for_form() (lines 142-149), exposing the PAT token to the frontend and any API client. The token should be stripped from responses sent to clients—only the server should retain it when executing git clone operations.

nano_press/templates/components/core/summary.html (1)

452-479: Add timeout to prevent indefinite polling.

pollForBuildCompletion polls indefinitely with no maximum attempts or timeout. If the build never completes or fails silently, this will poll forever, consuming resources and leaving the user stuck.

🔎 Proposed fix with timeout
         async pollForBuildCompletion(customImageName) {
+            const maxAttempts = 120; // 10 minutes at 5-second intervals
+            let attempts = 0;
             return new Promise((resolve, reject) => {
                 const pollInterval = setInterval(async () => {
                     try {
+                        attempts++;
+                        if (attempts >= maxAttempts) {
+                            clearInterval(pollInterval);
+                            reject(new Error('Build timed out after 10 minutes'));
+                            return;
+                        }
+
                         const url = '/api/method/nano_press.nano_press.doctype.custom_image.custom_image.get_build_status';
                         const payload = {
                             custom_image_name: customImageName
                         };

                         const response = await frappe_call(url, payload, "POST");

                         if (response?.message) {
                             const status = response.message.status;

                             if (status === 'Built') {
                                 clearInterval(pollInterval);
                                 resolve(customImageName);
                             } else if (status === 'Failed') {
                                 clearInterval(pollInterval);
                                 reject(new Error('Custom image build failed'));
                             }
                         }
                     } catch (error) {
                         console.error('Failed to poll build status:', error);
+                        // Stop polling after multiple consecutive failures
+                        if (attempts >= 3) {
+                            clearInterval(pollInterval);
+                            reject(new Error('Failed to check build status'));
+                        }
                     }
                 }, 5000);
             });
         },
🧹 Nitpick comments (10)
.github/workflows/ci.yml (1)

54-54: Update actions/setup-node to v4.

The actions/setup-node@v3 action is deprecated. Please update to v4 for continued support.

As per static analysis hints, the runner for actions/setup-node@v3 is too old.

🔎 Proposed fix
-      - name: Setup Node
-        uses: actions/setup-node@v3
+      - name: Setup Node
+        uses: actions/setup-node@v4
nano_press/www/index.py (1)

1-6: Consider clarifying the dual no_cache settings.

Both the module-level no_cache = 1 and context.no_cache = 1 inside get_context() serve the same purpose. If both are required by the framework, a brief comment explaining why would help future maintainers. Otherwise, one may be redundant.

🔎 Proposed clarification
+# Module-level no_cache prevents page caching at the route level
 no_cache = 1


 def get_context(context):
+	# Context-level no_cache ensures dynamic rendering on each request
 	context.no_cache = 1
 	return context
nano_press/templates/components/landing/features.html (1)

82-82: Minor: Inconsistent spacing in "Real time Logs".

Consider using "Real-time Logs" (hyphenated) for consistency with standard English usage.

nano_press/templates/includes/sidebar.html (1)

61-67: Consider POST for logout action.

Using a GET request (href) for logout is functional but not ideal since it's a state-changing action. Modern best practices recommend POST requests for logout to prevent CSRF and accidental logouts via prefetching. However, this follows Frappe's standard pattern.

nano_press/templates/layout.html (1)

56-71: Document the split-tag pattern between sidebar.html and layout.html.

The closing </div> tags on lines 69-71 close elements opened in sidebar.html (which opens 10 divs but closes only 7). This split-tag pattern across files is fragile—changes to either file's div structure can break the layout. Add inline comments documenting this dependency in both files.

nano_press/templates/components/core/domain.html (1)

23-30: Consider hiding DNS instructions when server IP is unavailable.

The fallback '0.0.0.0' could mislead users into creating an A record pointing to an invalid IP. Consider hiding the DNS instructions entirely until a valid server IP is available, or displaying a more explicit placeholder like "Server IP not configured".

🔎 Proposed enhancement
-    <div x-show="$store.installer.domain && $store.installer.domain.trim()" class="mt-3">
+    <div x-show="$store.installer.domain && $store.installer.domain.trim() && $store.installer.serverDetails?.ip" class="mt-3">
       <div class="bg-blue-50 border border-blue-200 rounded-md p-4">
         <p class="text-sm text-gray-700 mb-2">Point your domain to the server by creating an <span class="font-semibold">A record</span>:</p>
         <p class="font-mono text-sm text-gray-900 font-medium">
-          <span x-text="$store.installer.domain"></span> → <span x-text="$store.installer.serverDetails?.ip || '0.0.0.0'"></span>
+          <span x-text="$store.installer.domain"></span> → <span x-text="$store.installer.serverDetails?.ip"></span>
         </p>
       </div>
     </div>
+    <div x-show="$store.installer.domain && $store.installer.domain.trim() && !$store.installer.serverDetails?.ip" class="mt-3">
+      <div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
+        <p class="text-sm text-yellow-700">Please verify your server first to see DNS instructions.</p>
+      </div>
+    </div>
nano_press/templates/components/core/summary.html (1)

128-141: Consider adding a password visibility toggle.

The admin password is displayed in plain text, which could be a security concern during screen-sharing or if someone is looking over the user's shoulder. Consider adding a show/hide toggle for the password field.

nano_press/public/js/script.js (1)

71-120: Consider preventing duplicate app names.

The addCustomApp() function doesn't check if an app with the same name already exists in customApps. This could lead to duplicate entries that might cause issues during deployment.

🔎 Proposed enhancement
 addCustomApp() {
     // Clear all errors
     this.formErrors = { name: '', githubUrl: '', token: '', branch: '' };

     let hasError = false;

     // Validate all required fields
     if (!this.customAppForm.name) {
         this.formErrors.name = 'App name is required.';
         hasError = true;
+    } else if (this.customApps.some(app => app.name.toLowerCase() === this.customAppForm.name.toLowerCase())) {
+        this.formErrors.name = 'An app with this name already exists.';
+        hasError = true;
     }
nano_press/templates/components/core/apps.html (2)

231-277: Duplicate validation logic with script.js.

The addCustomApp() method here duplicates the validation logic in nano_press/public/js/script.js (lines 71-120). This creates maintenance burden and risk of divergence. Consider consolidating the validation logic in one place and referencing it from both components.


206-218: Duplicate state definitions with script.js.

formErrors, appType, and localCustomAppForm structures are duplicated here and in script.js. While component-local state is valid, consider whether the installer store (window.frappeInstaller) could serve as the single source of truth for these form states, with components accessing them via $store.installer.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f91367 and 07b5bc6.

⛔ Files ignored due to path filters (1)
  • nano_press/public/images/nano-press.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • .github/workflows/ci.yml
  • nano_press/hooks.py
  • nano_press/nano_press/doctype/apps/apps.json
  • nano_press/nano_press/doctype/apps/apps.py
  • nano_press/nano_press/doctype/custom_image/custom_image.py
  • nano_press/nano_press/utils/ansible/playbooks/build_custom_image.yml
  • nano_press/public/css/theme.css
  • nano_press/public/js/script.js
  • nano_press/public/js/vendor/alpinejs/plugins/mask.js
  • nano_press/templates/components/core/apps.html
  • nano_press/templates/components/core/domain.html
  • nano_press/templates/components/core/stepper.html
  • nano_press/templates/components/core/summary.html
  • nano_press/templates/components/landing/features.html
  • nano_press/templates/components/landing/hero.html
  • nano_press/templates/includes/home/custom_app_modal.html
  • nano_press/templates/includes/sidebar.html
  • nano_press/templates/includes/toast-teleport.html
  • nano_press/templates/layout.html
  • nano_press/templates/layout_public.html
  • nano_press/www/index.html
  • nano_press/www/index.py
  • nano_press/www/self_host.py
🧰 Additional context used
🧬 Code graph analysis (2)
nano_press/public/js/vendor/alpinejs/plugins/mask.js (1)
nano_press/public/js/vendor/alpinejs/core.js (1)
  • i (5-5)
nano_press/nano_press/doctype/apps/apps.py (2)
nano_press/nano_press/doctype/frappe_site/frappe_site.py (2)
  • before_insert (43-44)
  • validate (62-65)
nano_press/nano_press/doctype/server/server.py (1)
  • validate (43-48)
🪛 actionlint (1.7.9)
.github/workflows/ci.yml

54-54: the runner of "actions/setup-node@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🔇 Additional comments (30)
.github/workflows/ci.yml (2)

48-51: LGTM: Python 3.14 upgrade is valid.

Python 3.14 was released on October 7, 2025, with the latest maintenance release being 3.14.2 from December 5, 2025. This upgrade is appropriate and brings improvements including free-threaded Python support, deferred annotation evaluation, and template string literals.


53-57: LGTM: Node.js 24 upgrade is valid.

Node.js 24 was released in May 2025 with V8 13.6 and npm 11, and entered Long Term Support (LTS) in October 2025 with support continuing through April 2028. This upgrade brings performance improvements, AsyncContextFrame for AsyncLocalStorage, and improved HTTP capabilities via Undici 7.

nano_press/templates/components/core/stepper.html (1)

8-8: LGTM – z-index adjustment addresses layering.

The reduction from z-index 10 to 0 aligns with the PR objective to fix stepper overlap issues with the navbar.

nano_press/templates/includes/toast-teleport.html (1)

343-343: Critical UX fix: success toasts now display green.

This corrects a critical visual bug where success messages were incorrectly styled with red text (text-red-900), which typically indicates errors or warnings. The new green color (text-green-600) provides appropriate positive feedback to users.

nano_press/nano_press/utils/ansible/playbooks/build_custom_image.yml (1)

32-42: Improved reliability with fresh clone approach.

The two-step flow (remove directory, then fresh clone) ensures a pristine checkout and eliminates risks of stale files or partial state from previous runs. This is more robust than the prior update-with-force approach, though it trades some speed for reliability.

nano_press/hooks.py (2)

71-71: LGTM – home page now points to the new landing page.

Changing from "login" to "index" aligns with the PR's introduction of a public landing page at nano_press/www/index.html.


154-160: Consider deleting or documenting the disabled scheduler.

The commented scheduler_events containing ping_server will no longer execute automatic health checks every 5 minutes. The ping_server functions remain in nano_press/utils/ansible_runner.py and nano_press/api.py and are still accessible via the API, so they can be called manually if needed. However, keeping commented code can cause confusion. Either delete it permanently or add a TODO comment explaining why it's disabled and when it should be re-enabled.

nano_press/www/self_host.py (1)

13-18: LGTM – additional field supports enhanced app management.

Including the frappe field in the query aligns with the PR's broader app management enhancements, providing additional metadata for distinguishing Frappe apps from custom apps.

nano_press/www/index.html (2)

1-36: Landing page template looks good.

The template is well-structured with:

  • Proper Jinja2 inheritance from the public layout
  • Semantic HTML sections
  • Accessible CTAs with descriptive text and icons
  • Clean Tailwind CSS styling

24-24: No action needed on hardcoded GitHub URL.

The URL is correct and consistent across all three occurrences in the codebase (index.html, hero.html, footer.html). Hardcoding a project's own GitHub repository link is standard practice and doesn't require configuration via environment variables or site settings.

Likely an incorrect or invalid review comment.

nano_press/nano_press/doctype/apps/apps.py (2)

9-29: Auto-generated type hints placement is unconventional but functional.

The TYPE_CHECKING block is placed inside the class body, which is unusual but works in Python. This pattern is likely generated by Frappe's tooling for DocType type hints.


34-39: Incomplete handling when is_custom is False.

The validate() method only sets is_public when is_custom is truthy. If is_custom is False (i.e., a Frappe app), is_public remains unchanged. Verify this is intentional—if Frappe apps should always be public, consider explicitly setting is_public = 1 for that case.

🔎 Suggested fix if Frappe apps should always be public
 def validate(self):
     if self.is_custom:
         if self.pat_token:
             self.is_public = 0
         else:
             self.is_public = 1
+    else:
+        # Non-custom (Frappe) apps are always public
+        self.is_public = 1
nano_press/templates/includes/home/custom_app_modal.html (2)

7-27: LGTM! Good accessibility implementation.

The tabbed UI correctly implements ARIA roles (tablist, tab) and aria-selected states for screen reader support. The conditional styling provides clear visual feedback.


46-52: Verify token field visibility matches validation logic.

The GitHub Token field is shown when appType === 'private'. Ensure the corresponding addCustomApp() validation in script.js requires the token only when appType === 'private' to avoid validation mismatches.

nano_press/templates/components/landing/hero.html (1)

35-56: Nice terminal demo for the landing page!

The simulated terminal output provides a clear visual of the deployment experience. The animated cursor adds a polished touch.

nano_press/templates/layout.html (1)

24-24: LGTM - mask.js plugin added.

The Alpine.js mask plugin is correctly included after the other plugins and before the core script.

nano_press/templates/includes/sidebar.html (2)

4-6: Good accessibility: skip-to-content link implemented.

The skip link allows keyboard and screen reader users to bypass navigation and jump directly to main content.


44-71: Good keyboard navigation support.

The profile menu properly implements keyboard navigation with x-on:keydown.down.prevent and x-on:keydown.up.prevent for arrow key navigation, plus x-trap for focus management.

nano_press/nano_press/doctype/custom_image/custom_image.py (2)

88-103: Good defensive handling for missing repo_url.

The guard against missing repo_url with a default empty string prevents AttributeError. The updated logic using not is_public aligns with the new data model.


369-382: LGTM - Apps payload updated correctly.

The field renames (app_name, pat_token) and new fields (enabled, frappe) align with the Apps DocType schema changes.

nano_press/public/js/vendor/alpinejs/plugins/mask.js (1)

1-1: Vendor code - no review required.

This is a minified third-party Alpine.js mask plugin. Ensure it's sourced from the official Alpine.js plugins repository or a trusted source for security.

nano_press/templates/components/core/domain.html (2)

69-74: Condition in updateDomain may not work as intended.

The check value !== this.$store.installer.domain compares the new value against the current store value before updating it. This means if the user types, the condition will almost always be true (since value is the new input and this.$store.installer.domain is the old value). The domainVerified flag should be reset whenever the domain changes from its verified state, but the current condition checks domainVerified && value !== ... which is correct logic.

However, note that line 73 updates this.$store.installer.domain = value AFTER the check, so the comparison is actually correct (comparing new vs old). No change needed here.


80-118: LGTM!

The nextStep() logic correctly enforces domain verification every time the user proceeds, which is a good defensive approach. The error handling and state management are well-structured.

nano_press/templates/components/core/summary.html (1)

86-107: LGTM!

The step indicator logic correctly shows yellow for the current step and green for completed steps. The conditional classes are well-structured and consistent across all deployment phases.

nano_press/public/js/script.js (1)

26-38: LGTM!

The form state structure with formErrors and customAppForm (including the new branch field) provides good support for inline validation and error messaging.

nano_press/nano_press/doctype/apps/apps.json (2)

55-61: LGTM!

Good changes: changing pat_token to Password fieldtype improves security by masking the value, and the updated depends_on correctly ties the token visibility to custom apps (is_custom == 1).


92-101: LGTM!

The new is_custom field provides a clear distinction between Frappe apps and custom apps, enabling the filtering logic in the UI.

nano_press/templates/components/core/apps.html (3)

26-39: LGTM!

The Frappe Apps grid with toggle functionality and conditional styling for selected state is well-implemented. The case-insensitive comparison using toLowerCase() is applied consistently.


100-121: Good accessibility implementation.

The tab implementation properly uses ARIA roles (role="tablist", role="tab") and aria-selected attributes for the Public/Private app type selection. This enhances keyboard navigation and screen reader support.


192-199: LGTM!

The computed getters frappeApps and customApps provide clean filtering based on the frappe field. This separates apps into their respective sections in the UI.

Comment on lines +12 to +36
/* Penguin UI Design Tokens */
--color-primary: oklch(45.56% 0.237 264.05);
--color-primary-dark: oklch(70% 0.2 264.05);

/* Surface colors */
--color-surface: oklch(100% 0 0);
--color-surface-alt: oklch(98% 0 0);
--color-surface-dark: oklch(23% 0.011 285.82);
--color-surface-dark-alt: oklch(20% 0.011 285.82);

/* Text colors */
--color-on-surface: oklch(40% 0.019 285.88);
--color-on-surface-strong: oklch(20% 0.019 285.88);
--color-on-surface-dark: oklch(90% 0.005 285.82);
--color-on-surface-dark-strong: oklch(100% 0 0);
--color-on-primary: oklch(100% 0 0);
--color-on-primary-dark: oklch(20% 0.019 285.88);

/* Border colors */
--color-outline: oklch(85% 0.005 285.82);
--color-outline-dark: oklch(30% 0.011 285.82);

/* Border radius */
--radius: 0.5rem;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

oklch CSS color format browser support compatibility

💡 Result:

Short summary

  • OKLCH (oklch()) is widely supported in modern browsers but not universal; most current desktop/mobile releases of Chrome, Edge and Firefox, and recent Safari versions support it, while older browsers and some WebView/Samsung-Internet builds may not. [1][2][3]

Key support details

  • Can I Use shows broad support across modern Chrome, Edge and Firefox releases and Safari (check specific versions for your audience). It also lists gaps for older releases, some Android/WebView variants, and a few niche browsers. [2]
  • MDN documents the oklch() syntax and compatibility notes. [1]
  • Guides recommend providing fallbacks because some users run older browsers or un-updated devices (e.g., older Samsung Internet or legacy Android). [3]

Practical fallback pattern (use before the OKLCH value)

  • Provide an sRGB/HEX/RGB fallback, then override inside an @supports block that checks OKLCH support. Example:
    background: #6495ed; /* fallback */
    @supports (color: oklch(0% 0 0)) {
    background: oklch(70% 0.15 240);
    }
    (Adapt values as needed.) [3]

If you want, I can:

  • look up exact minimum browser versions for a specific audience (desktop/mobile %, or by country) and give a precise compatibility table, or
  • generate PostCSS/Build-tool fallbacks or polyfill recommendations.

Add fallback values for oklch() colors.

The oklch() format lacks universal browser support—older browsers and some Android/WebView variants do not support it. Provide fallback values using standard CSS colors (hex, RGB) before each oklch declaration, or wrap oklch declarations in an @supports rule that checks for color format support.

Example pattern:

--color-primary: #6495ed; /* fallback */
@supports (color: oklch(0% 0 0)) {
  --color-primary: oklch(45.56% 0.237 264.05);
}

This ensures older browsers can still render the design using fallback colors.

🤖 Prompt for AI Agents
nano_press/public/css/theme.css lines 12-36: the CSS currently uses only oklch()
color functions which lack universal browser support; add fallback color values
(hex or rgb) before each oklch() variable or wrap the oklch() assignments in
@supports(color: oklch(...)) blocks so browsers that don’t understand oklch will
use the fallback. For each variable, insert a conservative hex/rgb fallback on
the same property first (e.g. --color-primary: #xxxxxx;) then override with the
oklch() inside an @supports rule (or simply place the fallback before the
oklch() definition) and repeat for all oklch() variables (primary, surface,
text, outline, etc.), choosing visually close fallback values and keeping the
same variable names and ordering so existing usage is unchanged.

</div>

<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="group p-8 from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
Copy link

@coderabbitai coderabbitai bot Dec 27, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing gradient direction class.

The class from-gray-50 to-white won't produce a gradient without a bg-gradient-to-* direction class (e.g., bg-gradient-to-b). This also applies to lines 25 and 37.

🔎 Proposed fix
-<div class="group p-8 from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
+<div class="group p-8 bg-gradient-to-b from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">

Apply similarly to lines 25 and 37.

📝 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
<div class="group p-8 from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="group p-8 bg-gradient-to-b from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
🤖 Prompt for AI Agents
In nano_press/templates/components/landing/features.html around lines 13, 25 and
37 the utility classes use "from-gray-50 to-white" but are missing a gradient
direction class so no gradient will render; add an appropriate Tailwind gradient
direction class (for example "bg-gradient-to-b" or "bg-gradient-to-r" as design
requires) to each of those divs so the from/to colors produce a visible
gradient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need gradient

Copy link

Choose a reason for hiding this comment

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

@Rl0007, understood! I'll skip the gradient suggestion. If those from-* and to-* classes aren't needed, feel free to remove them to keep the markup clean, but that's entirely up to you. 😊

Comment on lines +25 to +32
<a href="https://github.com/BuildWithHussain/nano_press"
target="_blank"
class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-lg hover:border-gray-400 transition-all duration-200 shadow-md hover:shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to external link.

External links with target="_blank" should include rel="noopener noreferrer" to prevent potential tabnabbing attacks and improve performance.

🔎 Proposed fix
 <a href="https://github.com/BuildWithHussain/nano_press"
    target="_blank"
+   rel="noopener noreferrer"
    class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-lg hover:border-gray-400 transition-all duration-200 shadow-md hover:shadow-lg">
📝 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
<a href="https://github.com/BuildWithHussain/nano_press"
target="_blank"
class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-lg hover:border-gray-400 transition-all duration-200 shadow-md hover:shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
<a href="https://github.com/BuildWithHussain/nano_press"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center px-8 py-4 text-lg font-semibold text-gray-700 bg-white border-2 border-gray-300 rounded-lg hover:border-gray-400 transition-all duration-200 shadow-md hover:shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
🤖 Prompt for AI Agents
In nano_press/templates/components/landing/hero.html around lines 25 to 32 the
external GitHub anchor uses target="_blank" but is missing rel="noopener
noreferrer"; update the <a> tag to include rel="noopener noreferrer" to prevent
tabnabbing and improve performance while keeping the existing attributes intact.

Comment on lines +1 to +2
{% set user_image = frappe.db.get_value("User", frappe.session.user, "user_image") or "/assets/frappe/images/default-avatar.png" %}
{% set full_name = frappe.utils.get_fullname(frappe.session.user) if frappe.utils.get_fullname else frappe.session.user %}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Conditional check may not work as intended.

The condition if frappe.utils.get_fullname checks if the function exists, not if it returns a non-empty value. The fallback to frappe.session.user would only trigger if get_fullname is undefined, not if it returns an empty string.

🔎 Proposed fix
 {% set user_image = frappe.db.get_value("User", frappe.session.user, "user_image") or "/assets/frappe/images/default-avatar.png" %}
-{% set full_name = frappe.utils.get_fullname(frappe.session.user) if frappe.utils.get_fullname else frappe.session.user %}
+{% set full_name = frappe.utils.get_fullname(frappe.session.user) or frappe.session.user %}
📝 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
{% set user_image = frappe.db.get_value("User", frappe.session.user, "user_image") or "/assets/frappe/images/default-avatar.png" %}
{% set full_name = frappe.utils.get_fullname(frappe.session.user) if frappe.utils.get_fullname else frappe.session.user %}
{% set user_image = frappe.db.get_value("User", frappe.session.user, "user_image") or "/assets/frappe/images/default-avatar.png" %}
{% set full_name = frappe.utils.get_fullname(frappe.session.user) or frappe.session.user %}
🤖 Prompt for AI Agents
In nano_press/templates/includes/sidebar.html around lines 1-2, the current
conditional uses "if frappe.utils.get_fullname" which only checks existence of
the function, not its return value; replace this with calling get_fullname and
falling back on frappe.session.user when the result is empty or falsy (e.g.,
assign full_name = frappe.utils.get_fullname(frappe.session.user) or
frappe.session.user) so the fallback triggers when get_fullname returns an empty
string or None.

Comment on lines +22 to +28
</head>

<style>
[x-cloak] {
display: none !important;
}
</style>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move <style> block inside <head>.

The <style> block for x-cloak is placed between </head> and <body>, which is invalid HTML structure. Move it inside the <head> element, preferably after the stylesheet link.

🔎 Proposed fix
     <link rel="stylesheet" href="/assets/nano_press/css/tailwind.css">

+    <style>
+        [x-cloak] {
+            display: none !important;
+        }
+    </style>
+
     <script defer src="/assets/nano_press/js/vendor/alpinejs/core.js"></script>
     {% block head %} {% endblock %}
 </head>
-
-<style>
-    [x-cloak] {
-        display: none !important;
-    }
-</style>
📝 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
</head>
<style>
[x-cloak] {
display: none !important;
}
</style>
<link rel="stylesheet" href="/assets/nano_press/css/tailwind.css">
<style>
[x-cloak] {
display: none !important;
}
</style>
<script defer src="/assets/nano_press/js/vendor/alpinejs/core.js"></script>
{% block head %} {% endblock %}
</head>
<body>
🤖 Prompt for AI Agents
In nano_press/templates/layout_public.html around lines 22 to 28 the inline
<style> block defining [x-cloak] is placed after the closing </head>, which is
invalid; move that entire <style> block inside the <head> element (preferably
immediately after the stylesheet link) so it becomes part of the document head
and remove it from between </head> and <body> to restore valid HTML structure.

Comment on lines +35 to +38
{% include "templates/includes/footer.html" %}
</body>
{% block script %} {% endblock %}
</html>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move script block inside <body>.

The {% block script %} is placed after </body>, which is invalid HTML structure. Move it inside the <body> element, after the footer include.

🔎 Proposed fix
     {% include "templates/includes/footer.html" %}
+    {% block script %} {% endblock %}
 </body>
-{% block script %} {% endblock %}
 </html>
📝 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
{% include "templates/includes/footer.html" %}
</body>
{% block script %} {% endblock %}
</html>
{% include "templates/includes/footer.html" %}
{% block script %} {% endblock %}
</body>
</html>
🤖 Prompt for AI Agents
In nano_press/templates/layout_public.html around lines 35 to 38, the {% block
script %} is placed after </body>, producing invalid HTML; move the block inside
the <body> element immediately after the {% include
"templates/includes/footer.html" %} line and before the </body> tag so scripts
are rendered inside the document body, then close </body> and </html> as before.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (4)
nano_press/nano_press/doctype/app_version/app_version.py (1)

8-11: Consider adding explicit validation for edge cases.

The autoname method relies on the truthiness of self.version. While the field is marked as required in the DocType schema, adding explicit validation would make the code more defensive against edge cases like empty strings or whitespace-only values.

🔎 Proposed defensive validation
 def autoname(self):
-	if self.version:
+	if self.version and self.version.strip():
 		self.scrubbed_version = self.version.lower().replace(" ", "-")
 		self.name = self.scrubbed_version
+	else:
+		frappe.throw("Version cannot be empty")
nano_press/templates/components/core/apps.html (1)

238-268: Consider refactoring validation for maintainability.

The validation logic is comprehensive and correct, but the multiple if blocks make it verbose. Consider extracting validation rules into a schema or helper functions for better maintainability.

🔎 Example validation refactor
+validateCustomAppForm() {
+    const rules = {
+        name: { required: true, message: 'App name is required.' },
+        githubUrl: { 
+            required: true, 
+            message: 'GitHub URL is required.',
+            pattern: /^https:\/\//,
+            patternMessage: 'GitHub URL must start with https://'
+        },
+        branch: { required: true, message: 'Branch is required.' },
+        token: { 
+            required: this.appType === 'private',
+            message: 'GitHub Token is required for private repositories.'
+        }
+    };
+    
+    let hasError = false;
+    for (const [field, rule] of Object.entries(rules)) {
+        if (rule.required && !this.localCustomAppForm[field]) {
+            this.formErrors[field] = rule.message;
+            hasError = true;
+        } else if (rule.pattern && !rule.pattern.test(this.localCustomAppForm[field])) {
+            this.formErrors[field] = rule.patternMessage;
+            hasError = true;
+        }
+    }
+    return !hasError;
+},
+
 addCustomApp() {
     this.formErrors = { name: '', githubUrl: '', token: '', branch: '' };
-    let hasError = false;
-    // ... rest of validation ...
-    if (hasError) {
+    if (!this.validateCustomAppForm()) {
         return;
     }
nano_press/nano_press/doctype/custom_image/custom_image.py (2)

79-100: Clarify exception handling strategy.

The exception handler at lines 96-100 performs rollback, updates status, logs the error, then re-raises the exception. This pattern is unclear:

  • The status update at line 81 already committed via _update_status (line 104), so the rollback won't undo it
  • Re-raising the exception means callers still see the failure
  • The rollback and status update may not provide the intended safety net

Consider either:

  1. Fully handle the exception and return an error status (don't re-raise), or
  2. Remove the exception handler and let failures propagate naturally (move cleanup to a finally block if needed)

156-188: Consider HTML escaping in email messages.

The broad exception handling at line 187 is appropriate for notification code (similar to line 153).

However, the HTML email messages at lines 164-178 use f-strings with user-controlled values (self.image_name, self.image_tag, etc.) without escaping. While less critical in email context than web XSS, escaping HTML entities would improve correctness and prevent potential rendering issues.

🔎 Optional: Add HTML escaping
+from html import escape

 def _send_email_notification(self, status):
 	try:
 		recipient = frappe.db.get_value("User", self.owner, "email")
 		if not recipient or "@" not in recipient:
 			return
 
 		if status == "success":
 			subject = _("Custom Image Build Successful - {0}").format(self.image_name)
 			message = f"""
-			<p>Your custom Docker image <strong>{self.image_name}</strong> has been built successfully.</p>
-			<p><strong>Image Tag:</strong> {self.image_tag}</p>
-			<p><strong>Server:</strong> {self.server_name}</p>
-			<p><strong>Frappe Version:</strong> {self.frappe_version}</p>
-			<p><strong>Build Duration:</strong> {self.build_duration or 0} seconds</p>
+			<p>Your custom Docker image <strong>{escape(self.image_name)}</strong> has been built successfully.</p>
+			<p><strong>Image Tag:</strong> {escape(self.image_tag)}</p>
+			<p><strong>Server:</strong> {escape(self.server_name)}</p>
+			<p><strong>Frappe Version:</strong> {escape(str(self.frappe_version))}</p>
+			<p><strong>Build Duration:</strong> {escape(str(self.build_duration or 0))} seconds</p>
 			"""
 		else:
 			subject = _("Custom Image Build Failed - {0}").format(self.image_name)
 			message = f"""
 			<p>Your custom Docker image build encountered an error.</p>
-			<p><strong>Image Name:</strong> {self.image_name}</p>
-			<p><strong>Server:</strong> {self.server_name}</p>
+			<p><strong>Image Name:</strong> {escape(self.image_name)}</p>
+			<p><strong>Server:</strong> {escape(self.server_name)}</p>
 			<p>Please check the build log for more details.</p>
 			"""
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07b5bc6 and 6ee1099.

📒 Files selected for processing (10)
  • nano_press/nano_press/doctype/app_version/__init__.py
  • nano_press/nano_press/doctype/app_version/app_version.json
  • nano_press/nano_press/doctype/app_version/app_version.py
  • nano_press/nano_press/doctype/apps/apps.json
  • nano_press/nano_press/doctype/custom_image/custom_image.json
  • nano_press/nano_press/doctype/custom_image/custom_image.py
  • nano_press/templates/components/core/apps.html
  • nano_press/templates/components/core/server.html
  • nano_press/www/self-host.html
  • nano_press/www/self_host.py
🧰 Additional context used
🪛 GitHub Actions: Linters
nano_press/nano_press/doctype/custom_image/custom_image.py

[error] 104-104: Semgrep blocking finding: frappe-manual-commit. Manually committing a transaction is highly discouraged. If manual commit is required then add a comment explaining why and '# nosemgrep' on the same line.

🪛 Ruff (0.14.10)
nano_press/nano_press/doctype/custom_image/custom_image.py

153-153: Do not catch blind exception: Exception

(BLE001)


187-187: Do not catch blind exception: Exception

(BLE001)


212-212: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (21)
nano_press/www/self-host.html (1)

34-41: LGTM! Version string standardization.

The change from 'Version-15' to 'version-15' aligns with the new App Version DocType's scrubbed_version format, ensuring consistency across the application.

nano_press/nano_press/doctype/custom_image/custom_image.json (1)

31-36: LGTM! Improved version management.

Changing the frappe_version field to link to the App Version DocType improves maintainability by centralizing version definitions and eliminating hardcoded version strings.

nano_press/www/self_host.py (3)

6-12: LGTM! Enhanced context for UI features.

The additions to get_context properly expose server and version data to the template, supporting the new server selection and version management features.


15-21: LGTM! Secure server filtering.

The get_user_servers function correctly filters servers by owner, ensuring users can only access their own server records.


24-37: LGTM! Expanded app and version data.

The updated get_apps and new get_frappe_versions functions properly expose the additional fields needed for the enhanced UI, including app logos and frappe app identification.

nano_press/nano_press/doctype/app_version/app_version.json (1)

1-48: LGTM! Well-structured DocType.

The App Version DocType is properly configured with a unique constraint on the version field and appropriate permissions. The "By script" naming rule aligns with the autoname method implementation.

nano_press/nano_press/doctype/apps/apps.json (1)

93-110: LGTM! Enhanced app metadata fields.

The new fields effectively support custom app management and visual branding. The is_custom field with default "0" properly distinguishes between Frappe and custom apps, and the app_logo integration with image_field enables proper image handling.

nano_press/templates/components/core/server.html (2)

14-33: LGTM! Server selection UI.

The server selection dropdown is well-implemented with conditional rendering and displays relevant server information (name, IP, status). The UI gracefully handles the case when no servers are available.


164-187: LGTM! Robust server selection logic.

The onServerSelect method properly:

  • Finds and populates server details into the installer state
  • Handles verification status appropriately
  • Uses defensive coding (checking toast existence, converting port to string)
  • Integrates cleanly with the existing verification flow
nano_press/templates/components/core/apps.html (2)

4-17: LGTM! Version selection updated.

The Frappe version selection correctly uses the new frappe_versions data, displaying the user-friendly version while using the normalized scrubbed_version for state management.


19-98: LGTM! Well-organized app sections.

The separation of Frappe apps and custom apps provides a clear user experience. The use of computed getters (frappeApps, customApps) keeps the logic clean, and the consistent UI treatment (logos/initials, branch display) enhances usability.

nano_press/nano_press/doctype/custom_image/custom_image.py (10)

15-17: LGTM!

The refactored before_save hook cleanly delegates to private methods for generating the apps JSON and setting the image tag.


23-47: LGTM!

The method properly validates app configuration, uses localized error messages via _(), and delegates URL building and sorting to focused helper methods.


49-51: LGTM!

Clean base64 encoding wrapper.


106-119: LGTM!

The success handler correctly calculates build duration, atomically updates multiple fields, and notifies the user.


121-124: LGTM!

Clean failure handling with status update, notification, and error logging.


126-128: LGTM!

Clean separation of notification channels.


130-154: LGTM!

The broad exception handling at line 153 (flagged by Ruff) is appropriate here—notification failures should be logged without disrupting the build flow. The error is properly logged at line 154.


190-200: LGTM!

Properly enqueues the build job with appropriate queue settings and enqueue_after_commit=True to ensure the document is saved before the job runs.


202-213: LGTM!

The broad exception handling at line 212 is appropriate—this preview endpoint should return graceful error responses rather than throwing exceptions. The error is captured and returned in the response dict.


272-279: LGTM!

Clean status getter with appropriate conditional return of image_tag only when build is complete.

Comment on lines 102 to 104
def _update_status(self, status):
frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
frappe.db.commit()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pipeline failure: manual commit requires justification.

Semgrep flagged line 104 for manually committing the transaction without explanation. Manual commits are discouraged in Frappe due to potential data consistency issues.

🔎 Proposed fix with justification comment
 def _update_status(self, status):
 	frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
-	frappe.db.commit()
+	# Manual commit needed: build runs in background job, status must be visible to UI immediately  # nosemgrep
+	frappe.db.commit()
📝 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
def _update_status(self, status):
frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
frappe.db.commit()
def _update_status(self, status):
frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
# Manual commit needed: build runs in background job, status must be visible to UI immediately # nosemgrep
frappe.db.commit()
🧰 Tools
🪛 GitHub Actions: Linters

[error] 104-104: Semgrep blocking finding: frappe-manual-commit. Manually committing a transaction is highly discouraged. If manual commit is required then add a comment explaining why and '# nosemgrep' on the same line.

🤖 Prompt for AI Agents
In nano_press/nano_press/doctype/custom_image/custom_image.py around lines
102-104, the explicit call to frappe.db.commit() (line 104) must be removed
because manual transaction commits are discouraged; modify the method to only
call frappe.db.set_value(...) and drop the frappe.db.commit() call, and if a
commit is truly required for a specific edge case, replace the removal with a
short justification comment and perform the commit in the caller/context where
transaction boundaries are explicit and documented.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
nano_press/templates/components/core/apps.html (1)

224-236: 🔴 CRITICAL: GitHub tokens still stored in localStorage.

The security issue flagged in the previous review remains unresolved. The addCustomApp method stores the GitHub token in localCustomApps, which is then synced to $store.installer.customApps via syncToStore() (line 235). This persists tokens to localStorage, creating a significant security risk:

  • localStorage is accessible to any JavaScript code, including third-party scripts
  • XSS vulnerabilities could expose these tokens
  • GitHub Personal Access Tokens grant significant permissions

Required action: Implement server-side token storage as recommended in the previous review.

🔎 Recommended approach (from previous review)
  1. Send the token to the backend immediately via a secure API call
  2. Store only a token reference ID (e.g., encrypted token ID) in localStorage
  3. Retrieve the actual token server-side when needed for operations
  4. Implement token encryption at rest on the server

Alternatively, avoid persisting tokens entirely and require users to re-enter them when needed.

🧹 Nitpick comments (3)
nano_press/nano_press/doctype/server/server.py (1)

244-244: Translation wrapper applied correctly, but consider consistency.

The error message is now translatable. However, lines 133-139 use frappe._() while this line uses _(). Both work, but using a consistent pattern throughout the file would improve maintainability.

💡 Optional: unify translation helper usage

Consider standardizing on one pattern throughout the file:

-		frappe.throw(frappe._("Traefik domain is required for Traefik deployment"))
+		frappe.throw(_("Traefik domain is required for Traefik deployment"))

Apply similarly to lines 135, 137, and 139.

nano_press/nano_press/utils/remote_builder.py (1)

11-11: LGTM! Validation messages properly internationalized.

All validation error messages in the build flow are now translatable. The implementation is clean and consistent.

Consider also wrapping the error message at line 65 for consistency:

frappe.throw(_("Server {0} is not verified. Please verify server connectivity first.").format(server_doc.name))

Also applies to: 25-25, 51-60

nano_press/templates/components/core/apps.html (1)

229-229: Redundant default value for branch.

The fallback || 'main' is redundant since localCustomAppForm.branch already defaults to 'main' at line 215.

🔎 Simplified code
-    branch: this.localCustomAppForm.branch || 'main'
+    branch: this.localCustomAppForm.branch
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ee1099 and 7249743.

📒 Files selected for processing (8)
  • nano_press/nano_press/doctype/frappe_site/frappe_site.py
  • nano_press/nano_press/doctype/server/server.py
  • nano_press/nano_press/utils/ansible/src/AnsibleRunner.py
  • nano_press/nano_press/utils/remote_builder.py
  • nano_press/public/js/script.js
  • nano_press/templates/components/core/apps.html
  • nano_press/templates/includes/home/custom_app_modal.html
  • nano_press/utils/ansible_runner.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • nano_press/public/js/script.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (7)
nano_press/nano_press/doctype/server/server.py (1)

8-8: LGTM! Translation import added correctly.

The import follows Frappe's standard i18n pattern.

nano_press/nano_press/doctype/frappe_site/frappe_site.py (1)

7-7: Excellent i18n implementation!

All error messages are properly wrapped for translation, and line 73 correctly uses .format() for the placeholder instead of f-strings, which is the recommended pattern for gettext-style translation.

Also applies to: 71-76

nano_press/utils/ansible_runner.py (1)

4-4: LGTM! API validation messages now translatable.

The translation wrappers are correctly applied to user-facing validation messages at the API boundary.

Also applies to: 34-34, 106-106

nano_press/nano_press/utils/ansible/src/AnsibleRunner.py (1)

17-17: LGTM! Connection validation errors now translatable.

The translation wrappers are properly applied to connection validation errors in both run_playbook and run_ping methods, maintaining consistency across the class.

Also applies to: 84-84, 144-144, 250-250

nano_press/templates/includes/home/custom_app_modal.html (1)

7-59: LGTM! Well-structured form with proper validation and accessibility.

The modal form is cleanly implemented with:

  • Proper ARIA attributes for the tabbed interface
  • HTML5 validation (required, pattern, type constraints)
  • Conditional token field based on app type
  • Clear labels and helpful placeholders
nano_press/templates/components/core/apps.html (2)

198-204: Good use of computed getters to separate Frappe and custom apps.

The getters cleanly split the available apps based on the frappe field, making the template logic simpler and more maintainable.


252-260: Good state synchronization pattern.

The syncToStore() method provides a clean way to keep local component state in sync with the central store, and it's properly called after each state mutation (in removeCustomApp, toggleApp, selectFrappeVersion, and addCustomApp).

…nagement

Security Fixes:
- Change pat_token field from Data to Password for encrypted storage
- Add register_custom_app() API to securely handle token storage
- Tokens no longer stored in localStorage (only reference IDs persist)
- Tokens cleared from client-side immediately after backend POST
- Add delete_custom_app() API to properly remove apps from backend
- Tokens encrypted at rest using Frappe's Password field
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
nano_press/templates/components/core/apps.html (1)

237-237: Case sensitivity concern for app names.

Converting app_name to lowercase may cause issues if the actual repository name or app package name is case-sensitive. This was flagged in a previous review. Consider preserving the original case or validating the normalized name against the actual repository structure.

🧹 Nitpick comments (2)
nano_press/nano_press/doctype/custom_image/custom_image.py (1)

153-154: Add specific exception types for better error handling.

The blind Exception catches in notification methods could mask unexpected errors. While notification failures shouldn't break the build flow, catching specific exceptions (e.g., frappe.ValidationError, SMTPException) would improve debugging.

🔎 Proposed refinement
 	except Exception:
-		frappe.log_error(title=_("Build Notification Failed"))
+		frappe.log_error(title=_("Build Notification Failed"), message=frappe.get_traceback())

Apply similar change to line 187-188.

Also applies to: 187-188

nano_press/api.py (1)

91-92: Add URL validation for github_url.

The endpoint validates presence of github_url but doesn't verify it's a valid GitHub URL format. Consider adding pattern validation to ensure it starts with https://github.com/ to catch input errors early.

🔎 Proposed validation
 	if not app_name or not github_url or not branch:
 		frappe.throw(_("app_name, github_url, and branch are required"))
+	
+	if not github_url.startswith("https://github.com/"):
+		frappe.throw(_("github_url must be a valid GitHub repository URL starting with https://github.com/"))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7249743 and f048dd1.

📒 Files selected for processing (4)
  • nano_press/api.py
  • nano_press/nano_press/doctype/apps/apps.json
  • nano_press/nano_press/doctype/custom_image/custom_image.py
  • nano_press/templates/components/core/apps.html
🧰 Additional context used
🧬 Code graph analysis (1)
nano_press/nano_press/doctype/custom_image/custom_image.py (2)
nano_press/nano_press/utils/ansible/src/AnsibleRunner.py (1)
  • run_playbook (60-120)
nano_press/utils/ansible_runner.py (1)
  • run_playbook (11-84)
🪛 Ruff (0.14.10)
nano_press/api.py

128-128: Do not catch blind exception: Exception

(BLE001)


155-155: Do not catch blind exception: Exception

(BLE001)

nano_press/nano_press/doctype/custom_image/custom_image.py

153-153: Do not catch blind exception: Exception

(BLE001)


187-187: Do not catch blind exception: Exception

(BLE001)


212-212: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (7)
nano_press/nano_press/doctype/apps/apps.json (2)

57-61: LGTM! Security improvement for token handling.

The change from Data to Password fieldtype for pat_token is a significant security enhancement, ensuring tokens are encrypted at rest. The conditional display logic (depends_on: "eval: doc.is_custom == 1") and added description properly guide users on when tokens are required.


93-110: LGTM! Well-structured field additions.

The new fields (app_logo, is_custom, column_break_eisq) follow Frappe conventions properly. The image_field linkage on Line 110 correctly associates the DocType with the app_logo field for image handling.

nano_press/api.py (1)

120-120: Manual commit appropriately documented.

The manual commit on line 120 is properly justified with a nosemgrep comment for the API endpoint's transactional boundary. Same applies to line 151.

nano_press/templates/components/core/apps.html (4)

243-270: LGTM! Significant security improvement for token handling.

The refactored custom app flow properly addresses the previous localStorage security concern. Tokens are now sent directly to the backend via the register_custom_app API and never persisted in client storage. The flow correctly:

  • Posts tokens immediately to the backend (line 244-253)
  • Stores only the app_reference_id reference (line 259)
  • Clears the form including the token field (line 274)
  • Handles errors appropriately with rollback on the backend

This is a substantial security enhancement.


159-166: LGTM! Proper conditional validation for private apps.

The token field is correctly marked as required only when appType === 'private' using the :required binding. This ensures private repositories have authentication while not burdening public repo users.


208-214: LGTM! Clean separation of Frappe and custom apps.

The computed getters (frappeApps and customApps) provide a clean separation based on the frappe field value, supporting the dual-section UI introduced in this PR.


280-306: LGTM! Proper cleanup with backend synchronization.

The removeCustomApp method correctly handles deletion by:

  • Calling the backend API to delete the app record if it has an app_reference_id
  • Providing user feedback via toast notifications
  • Gracefully handling backend failures while still removing the local entry
  • Syncing state after removal

- Fix Docker tag sanitization (handle special chars, leading dots/dashes)
- Fix app sorting bug with index alignment
- Optimize deployment vars to use pre-computed apps_json_base64
- Add server existence validation before build
- Validate custom app entries require name and repo_url
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
nano_press/nano_press/doctype/custom_image/custom_image.py (1)

71-84: Critical: Index mismatch bug in app sorting remains unfixed.

The method still assumes positional correspondence between apps_config indices and apps_list, but _generate_apps_json (lines 37-38) skips entries where app_item.app_name is None. This causes index misalignment.

Example of the bug:

  • apps_config = [item_no_name, itemA, itemB]
  • After _generate_apps_json, apps_list = [urlA, urlB] (only 2 items)
  • Line 82 with i=1 accesses apps_list[1] (urlB) but pairs it with itemA's order
  • Result: incorrect app ordering and potentially missing apps

The previous review comment on this issue recommended rebuilding apps_list in sorted order rather than relying on index matching.

🔎 Proposed fix: rebuild apps_list in correct order
 def _sort_apps_by_order(self, apps_list):
-	app_order_map = {}
+	# Build order map for all apps
+	order_map = {}
 	for item in self.apps_config:
 		if item.app_name:
-			order = frappe.db.get_value("Apps", item.app_name, "order") or 999
-			app_order_map[item.app_name] = order
-
-	sorted_apps = []
-	for i, app_item in enumerate(self.apps_config):
-		if app_item.app_name and i < len(apps_list):
-			order = app_order_map.get(app_item.app_name, 999)
-			sorted_apps.append((apps_list[i], order))
-
-	return [app for app, _ in sorted(sorted_apps, key=lambda x: x[1])]
+			order_map[item.app_name] = frappe.db.get_value("Apps", item.app_name, "order") or 999
+	
+	# Sort apps_config by order
+	sorted_config = sorted(
+		[item for item in self.apps_config if item.app_name],
+		key=lambda item: order_map.get(item.app_name, 999)
+	)
+	
+	# Rebuild apps_list in sorted order
+	sorted_list = []
+	for item in sorted_config:
+		app_doc = frappe.get_cached_doc("Apps", item.app_name)
+		sorted_list.append({
+			"url": self._build_repo_url(app_doc),
+			"branch": app_doc.branch,
+		})
+	
+	return sorted_list

This approach sorts apps_config first, then regenerates apps_list in the correct order, eliminating the index-based assumption.

🧹 Nitpick comments (3)
nano_press/nano_press/doctype/custom_image/custom_image.py (3)

19-29: Inconsistent character handling in sanitization.

Line 23 replaces whitespace with -, but line 24 removes other invalid characters entirely (empty string replacement). This creates inconsistent results: "My App@Name" becomes "my-appname" instead of "my-app-name".

Consider replacing invalid characters with - on line 24 to match the whitespace handling on line 23, ensuring consistent separator usage throughout the sanitized name.

🔎 Proposed fix for consistency
 def _set_image_tag(self):
 	import re
 
 	name = self.image_name.lower()
 	name = re.sub(r"\s+", "-", name)
-	name = re.sub(r"[^a-z0-9_.-]", "", name)
+	name = re.sub(r"[^a-z0-9_.-]", "-", name)
 	name = re.sub(r"[-_.]{2,}", "-", name)
 	name = name.lstrip(".-")
 
 	sanitized_name = name if name else "image"
 	self.image_tag = f"{sanitized_name}:latest"

116-118: Add justification comment for manual commit.

While the nosemgrep directive suppresses the linter, the code should include a brief comment explaining why manual commit is necessary here (e.g., "build runs in background job, status must be visible to UI immediately").

🔎 Proposed improvement
 def _update_status(self, status):
 	frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
-	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
+	# Manual commit needed: build runs in background job, status must be visible to UI immediately
+	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit

259-267: Strengthen custom app validation.

Lines 263-266 validate that app_name and repo_url are present, but don't check for empty strings or whitespace-only values. An empty "githubUrl": "" would pass the if not repo_url check but cause issues downstream.

🔎 Proposed fix with stricter validation
 for custom_app in custom_apps:
 	app_name = custom_app.get("name")
-	repo_url = custom_app.get("githubUrl")
+	repo_url = (custom_app.get("githubUrl") or "").strip()
 
-	if not app_name or not repo_url:
+	if not app_name or not repo_url:
 		frappe.throw(
 			_("Custom app must have both 'name' and 'githubUrl' fields. Received: {0}").format(custom_app)
 		)

This ensures githubUrl values like "" or " " are properly rejected.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f048dd1 and fe17d3c.

📒 Files selected for processing (1)
  • nano_press/nano_press/doctype/custom_image/custom_image.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:52.551Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.
📚 Learning: 2026-01-04T15:17:52.551Z
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:52.551Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.

Applied to files:

  • nano_press/nano_press/doctype/custom_image/custom_image.py
🪛 Ruff (0.14.10)
nano_press/nano_press/doctype/custom_image/custom_image.py

167-167: Do not catch blind exception: Exception

(BLE001)


201-201: Do not catch blind exception: Exception

(BLE001)


226-226: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (7)
nano_press/nano_press/doctype/custom_image/custom_image.py (7)

31-55: LGTM!

The apps JSON generation logic is well-structured with proper validation, efficient caching via get_cached_doc, and appropriate localization of error messages.


61-69: Verify that PAT tokens in URLs won't leak in logs.

The previous review flagged embedding authentication tokens in repository URLs as a security risk (tokens can appear in Ansible logs, build logs, and process listings). Although marked as addressed in commit f048dd1, tokens are still being embedded in HTTPS URLs on line 67.

Please confirm:

  1. This is the intended approach after security review
  2. Build logs and Ansible playbooks properly sanitize URLs containing credentials
  3. Git credential helpers or environment variables weren't viable alternatives

86-91: LGTM! Previous redundancy issue fixed.

The method now correctly uses the pre-computed self.apps_json_base64 value instead of regenerating it, addressing the performance concern from the previous review.


140-202: LGTM! Notification handling is robust.

The notification methods are well-structured with proper separation of concerns and defensive exception handling. Catching broad Exception types here is appropriate since notification failures should be logged but not disrupt the build process.


204-227: LGTM! Public API methods are well-implemented.

The enqueue_build_custom_image method properly queues long-running builds, and preview_apps_json_for_form provides safe preview functionality with appropriate error handling.


268-280: LGTM! Apps doc creation is correct for the secure flow.

The is_public=1 flag is appropriate here. Based on learnings, private apps with tokens are already registered in the database via the register_custom_app() API before this function is called. This fallback creation only executes for public apps that weren't pre-registered, making is_public=1 the correct default.

Based on learnings, this implements the secure token flow correctly.


294-302: LGTM! Build status endpoint is clean and appropriate.

The function provides a concise status snapshot with conditional inclusion of image_tag only when the build is complete.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
nano_press/nano_press/doctype/custom_image/custom_image.py (1)

116-118: Add explanatory comment for manual commit.

The nosemgrep directive is present, but the past review requested an inline justification explaining why the manual commit is necessary.

🔎 Suggested addition
 def _update_status(self, status):
 	frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
-	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
+	# Manual commit needed: build runs in background job, status must be visible to UI immediately
+	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
nano_press/templates/components/core/apps.html (2)

235-279: Consider adding duplicate app name validation.

The addCustomApp method doesn't check if an app with the same name already exists in localCustomApps before adding. This could lead to duplicate entries in the UI if a user accidentally submits the same app twice.

🔎 Suggested duplicate check

Add this check before line 242:

// Check for duplicate app names
const isDuplicate = this.localCustomApps.some(
    app => app.name.toLowerCase() === appData.name.toLowerCase()
);

if (isDuplicate) {
    if (typeof toast !== 'undefined') {
        toast(`App "${appData.name}" already exists`, { type: "warning" });
    }
    return;
}

21-69: LGTM! Consistent and accessible UI structure.

The split between Frappe Apps and Custom Apps sections provides clear organization. The card structure with logo/initial fallbacks and consistent styling creates a polished user experience.

Optional: The inline style="grid-template-columns: repeat(3, minmax(0, 1fr));" could be extracted to a CSS class for better maintainability if used in multiple places, but this is a minor point.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe17d3c and c66e8f4.

📒 Files selected for processing (2)
  • nano_press/nano_press/doctype/custom_image/custom_image.py
  • nano_press/templates/components/core/apps.html
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:52.551Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.
📚 Learning: 2026-01-04T15:17:52.551Z
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:52.551Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.

Applied to files:

  • nano_press/templates/components/core/apps.html
  • nano_press/nano_press/doctype/custom_image/custom_image.py
🪛 Ruff (0.14.10)
nano_press/nano_press/doctype/custom_image/custom_image.py

167-167: Do not catch blind exception: Exception

(BLE001)


201-201: Do not catch blind exception: Exception

(BLE001)


226-226: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (10)
nano_press/nano_press/doctype/custom_image/custom_image.py (7)

15-29: LGTM! Image tag sanitization is comprehensive.

The refactored before_save hook and _set_image_tag method correctly handle Docker tag constraints by sanitizing the image name through multiple regex passes and providing a safe fallback.


31-51: LGTM! Efficient apps JSON generation.

The use of frappe.get_cached_doc() on Line 39 is a good optimization for performance, and the validation ensures apps have complete configuration before proceeding.


63-84: LGTM! Sorting logic correctly addresses previous index mismatch.

The refactored _sort_apps_by_order method properly rebuilds the apps list in the correct order without relying on positional index alignment, resolving the previous concern about misalignment when app_name is None.


140-202: LGTM! Notification error handling is appropriate.

The broad exception catching in _send_realtime_notification (Line 167) and _send_email_notification (Line 201) is appropriate for graceful degradation—notification failures are logged but don't interrupt the build flow. The Ruff warnings are false positives in this context.


204-227: LGTM! Enqueue and preview methods are well-structured.

The enqueue_build_custom_image method follows Frappe's standard enqueue pattern. The broad exception handling in preview_apps_json_for_form (Line 226) is appropriate for a preview endpoint—it ensures the form receives structured error information rather than an unhandled exception. The Ruff warning is a false positive.


230-291: LGTM! Creation and validation logic is sound.

The create_and_build_custom_image function properly validates server existence and status (Lines 235-242), ensures custom apps have required fields (Lines 263-266), and correctly creates fallback Apps docs with is_public=1 (Lines 268-280). Based on learnings, this fallback only executes for public apps since private apps with tokens are pre-registered via register_custom_app(), making the is_public=1 default appropriate.


294-302: LGTM! Build status endpoint is straightforward.

The get_build_status function returns the essential build information with appropriate null handling for the image tag.

nano_press/templates/components/core/apps.html (3)

280-306: LGTM! Solid error handling and state management.

The removeCustomApp method correctly:

  • Attempts backend deletion only for apps that were registered (with app_reference_id)
  • Handles backend errors gracefully without blocking local state cleanup
  • Maintains consistency with the addCustomApp logic
  • Syncs changes to the store

318-326: LGTM! Clean state synchronization pattern.

The addition of syncToStore() and its integration into state-changing methods (toggleApp, selectFrappeVersion, addCustomApp, removeCustomApp) ensures consistent state management between local component state and the global store.


208-214: LGTM! Clean separation of app types.

The computed getters frappeApps and customApps provide a clear, reactive way to separate Frappe framework apps from custom apps based on the frappe flag.

Comment on lines +151 to +156
<input type="url" x-model="localCustomAppForm.githubUrl" placeholder="https://github.com/user/repo"
required
pattern="https://.*"
title="URL must start with https://"
class="w-full px-3 py-2.5 bg-white border border-gray-300 rounded-md text-gray-900 outline-none transition-all duration-200 text-sm font-sans focus:border-gray-900">
<p class="mt-1 text-xs text-gray-500">Must start with https://</p>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Strengthen GitHub URL validation pattern.

The current pattern https://.* accepts any HTTPS URL, not just GitHub repositories. This could allow invalid URLs that will fail during backend processing.

Consider using a more specific pattern that validates GitHub URLs:

🔎 Suggested validation pattern
-                        <input type="url" x-model="localCustomAppForm.githubUrl" placeholder="https://github.com/user/repo"
-                            required
-                            pattern="https://.*"
-                            title="URL must start with https://"
-                            class="w-full px-3 py-2.5 bg-white border border-gray-300 rounded-md text-gray-900 outline-none transition-all duration-200 text-sm font-sans focus:border-gray-900">
-                        <p class="mt-1 text-xs text-gray-500">Must start with https://</p>
+                        <input type="url" x-model="localCustomAppForm.githubUrl" placeholder="https://github.com/user/repo"
+                            required
+                            pattern="https://github\.com/.+"
+                            title="Must be a valid GitHub repository URL (https://github.com/user/repo)"
+                            class="w-full px-3 py-2.5 bg-white border border-gray-300 rounded-md text-gray-900 outline-none transition-all duration-200 text-sm font-sans focus:border-gray-900">
+                        <p class="mt-1 text-xs text-gray-500">Must be a GitHub repository URL</p>
🤖 Prompt for AI Agents
In nano_press/templates/components/core/apps.html around lines 151-156, the
input pattern `https://.*` is too permissive for GitHub repo URLs; replace it
with a stricter regex that only allows GitHub repository URLs (for example a
pattern that enforces the host and user/repo path, e.g. starting with
https://github.com/, followed by a non-empty user and repo segment, optionally
ending with .git or a trailing slash). Update the title and helper text to
reflect the stronger validation (e.g., "URL must be a GitHub repository like
https://github.com/user/repo"). Ensure the new pattern is anchored and escapes
dots so it validates client-side before submission.


localSelectedApps: [],
localFrappeVersion: 'Version-15',
localFrappeVersion: 'version-15',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded Frappe version default may not exist in available versions.

The default value 'version-15' might not exist in frappeVersionOptions if the backend doesn't provide it. This would leave the user with an invalid selection.

Consider defaulting to the first available version or the most recent version from frappeVersionOptions:

🔎 Suggested dynamic default
-            localFrappeVersion: 'version-15',
+            localFrappeVersion: '',

Then update the init() method:

             init() {
                 this.localSelectedApps = [...this.$store.installer.selectedApps];
-                this.localFrappeVersion = this.$store.installer.frappeVersion;
+                this.localFrappeVersion = this.$store.installer.frappeVersion || 
+                    (this.frappeVersionOptions.length > 0 ? this.frappeVersionOptions[0].scrubbed_version : '');
                 this.localCustomApps = [...this.$store.installer.customApps];
             },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In nano_press/templates/components/core/apps.html around line 217, the
localFrappeVersion is hardcoded to 'version-15', which may be absent from
frappeVersionOptions causing an invalid default; change the component to set
localFrappeVersion dynamically at initialization by selecting
frappeVersionOptions[0] or the newest/most appropriate entry (e.g., last or
based on a version sort) when the options array is present, and update init() to
assign this computed default only if the current value is missing or not found
in frappeVersionOptions.

Implement URL-based deployment tracking to preserve progress on page refresh.
When user refreshes during deployment, polling automatically resumes and
continues from current status. Removes blocking image build wait.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI Agents
In @nano_press/nano_press/doctype/frappe_site/frappe_site.json:
- Line 104: get_docker_image() currently returns custom_img.image_tag without
validating build_status, allowing deployments to use images that are still
"Building" or failed; update get_docker_image() to check custom_img.build_status
and only return custom_img.image_tag when build_status == "Built", otherwise
raise a user-facing error (or abort) indicating the custom image build is not
complete or failed so deployments cannot proceed with incomplete images.

In @nano_press/templates/components/core/summary.html:
- Around line 567-589: The summary view can be hidden because
$store.installer.currentStep isn't set when resuming a completed deployment;
update resumeCompletedDeployment (or init) to explicitly set
this.$store.installer.currentStep = 4 (or the step that shows the summary)
before setting deployComplete/deployStep so the installer store reflects the
summary state when ?complete=true&site=... is visited; modify the
resumeCompletedDeployment function (and/or init) to set currentStep early, then
continue fetching site data and credentials.
- Around line 80-99: The step indicator circles use 'text-gray-400' for the
current step (deployStep === n), which does nothing for a filled circle; update
each indicator div's :class ternary (the ones with "w-4 h-4 rounded-full
shrink-0") so that the middle branch (deployStep === n) applies a visible style
like 'bg-gray-500' or a ring/animation e.g. 'ring-2 ring-gray-900' or
'animate-pulse' instead of 'text-gray-400'; apply this change to every indicator
expression that currently uses (deployStep === X ? 'text-gray-400' :
'bg-gray-300') so the current step is visually distinct from incomplete steps.
- Around line 468-565: The polling interval created in resumeDeployment is
stored in a local const pollInterval and never saved on the component, so it
keeps running after navigation; change resumeDeployment to assign the interval
id to a component property (e.g., this.pollIntervalId) instead of the local
pollInterval, update every clearInterval(pollInterval) call to
clearInterval(this.pollIntervalId) and set this.pollIntervalId = null after
clearing, add pollIntervalId: null to the component data, and implement a
destroy() lifecycle method that checks and clears this.pollIntervalId to ensure
the interval is cleaned up when the component is destroyed.
🧹 Nitpick comments (1)
nano_press/templates/components/core/summary.html (1)

472-564: Consider adding a maximum poll duration or attempt limit.

The polling continues indefinitely until a terminal state is reached. If the server gets stuck in an intermediate state, this will poll forever. Consider adding a timeout (e.g., 30 minutes) or max attempts to prevent indefinite polling.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c66e8f4 and 6197375.

📒 Files selected for processing (2)
  • nano_press/nano_press/doctype/frappe_site/frappe_site.json
  • nano_press/templates/components/core/summary.html
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:56.591Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (4)
nano_press/templates/components/core/summary.html (4)

166-177: LGTM!

The URL parameter parsing for deployment resume is well-structured with proper conditional checks before calling the appropriate resume handler.


591-599: Verify status-to-step mapping alignment with deployment flow.

Comparing the step labels in the UI with the status mapping:

  • Step 3 = "Creating site" → but 'Ready To Deploy' -> 3 implies site creation is complete
  • Step 4 = "Preparing deployment" → but 'Deploying' -> 4
  • Step 5 = "Deploying site" → but 'Deployed' -> 5

The mapping may be off by one. Based on startDeployment(), 'Ready To Deploy' status occurs after site creation (step 3 complete), so it should map to step 4.

🔎 Suggested fix if mapping is confirmed incorrect
         getStepFromStatus(status) {
             const statusMap = {
-                'Not Deployed': 2,
-                'Ready To Deploy': 3,
-                'Deploying': 4,
+                'Not Deployed': 3,
+                'Ready To Deploy': 4,
+                'Deploying': 5,
                 'Deployed': 5
             };
             return statusMap[status] || 1;
         }

13-21: LGTM!

The conditional domain rendering with "Not configured" fallback provides clear user feedback and handles edge cases properly with the .trim() check.


225-226: URL state management approach is sound.

The replaceState calls enable deployment resume across page refreshes. Note that the /self-host path is hardcoded in multiple places — if the route ever changes, these would need updating. Consider extracting to a constant if this becomes a maintenance concern.

Also applies to: 261-262, 514-514, 536-536

"fieldtype": "Link",
"label": "Custom Image",
"link_filters": "[[\"Custom Image\",\"build_status\",\"=\",\"Built\"]]",
"link_filters": "[[\"Custom Image\",\"build_status\",\"in\",[\"Built\",\"Building\"]]]",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for deployment/provisioning logic related to custom_image status

echo "=== Searching for frappe_site.py file ==="
find . -name "frappe_site.py" -type f

echo -e "\n=== Searching for custom_image related code in frappe_site.py ==="
rg -n "custom_image" nano_press/nano_press/doctype/frappe_site/ --type=py -A 3 -B 3

echo -e "\n=== Searching for build_status checks in deployment code ==="
rg -n "build_status" . --type=py -A 2 -B 2

echo -e "\n=== Looking for deployment/provisioning methods ==="
rg -n "def.*deploy|def.*provision|def.*resume" nano_press/nano_press/doctype/frappe_site/ --type=py -A 5

echo -e "\n=== Checking for frappe.throw related to custom image or build status ==="
rg -n "frappe\.(throw|msgprint)" nano_press/nano_press/doctype/frappe_site/ --type=py -A 2 -B 1

Repository: BuildWithHussain/nano_press

Length of output: 11804


🏁 Script executed:

#!/bin/bash
# Check if there's any validation in prepare_for_deployment or deploy_site that checks custom_image build status

echo "=== Checking prepare_for_deployment method ==="
sed -n '141,175p' nano_press/nano_press/doctype/frappe_site/frappe_site.py

echo -e "\n=== Checking deploy_site method ==="
sed -n '175,210p' nano_press/nano_press/doctype/frappe_site/frappe_site.py

echo -e "\n=== Checking get_deployment_vars for any custom_image status validation ==="
sed -n '103,140p' nano_press/nano_press/doctype/frappe_site/frappe_site.py

echo -e "\n=== Checking get_docker_image more closely ==="
sed -n '92,102p' nano_press/nano_press/doctype/frappe_site/frappe_site.py

Repository: BuildWithHussain/nano_press

Length of output: 4157


Add validation to prevent deployment with incomplete custom images.

The link filter change allows selecting Custom Images with "Building" status, but the deployment code does not validate that the image is actually built. The get_docker_image() method (line 92-99) returns image_tag directly without checking build_status, and image_tag is set when the build starts, not when it completes. This allows deployments to proceed with incomplete Docker images, causing failures.

Add a check in get_docker_image() to verify build_status == "Built" before returning the image tag, or raise an error if the custom image build is still in progress or failed:

if custom_img.image_tag and custom_img.build_status == "Built":
    return custom_img.image_tag
🤖 Prompt for AI Agents
In @nano_press/nano_press/doctype/frappe_site/frappe_site.json at line 104,
get_docker_image() currently returns custom_img.image_tag without validating
build_status, allowing deployments to use images that are still "Building" or
failed; update get_docker_image() to check custom_img.build_status and only
return custom_img.image_tag when build_status == "Built", otherwise raise a
user-facing error (or abort) indicating the custom image build is not complete
or failed so deployments cannot proceed with incomplete images.

… deployment tracking

This update enables automatic status synchronization between Custom Image builds and Frappe Sites, allowing the frontend to track the entire deployment lifecycle by polling a single DocType.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @nano_press/nano_press/doctype/custom_image/custom_image.py:
- Around line 115-118: The manual commits flagged by semgrep are missing the
required inline justification; update both occurrences of frappe.db.commit() in
this file (the one inside Custom Image._update_status and the other
frappe.db.commit call later in the file) to keep the existing "nosemgrep:
frappe-semgrep-rules.rules.frappe-manual-commit" annotation and add a brief
same-line comment explaining why the manual commit is necessary (e.g., to ensure
the build_status change is persisted immediately before calling
_sync_status_to_frappe_sites or before subsequent cross-site operations), so the
comment sits on the same line as frappe.db.commit() and references the reason
for immediate DB persistence.

In @nano_press/nano_press/doctype/frappe_site/frappe_site.py:
- Around line 103-117: The validate_custom_image method currently only warns on
failed builds using frappe.msgprint, allowing submission to proceed; change the
behavior to block invalid state by replacing the msgprint with frappe.throw when
custom_image.build_status == "Failed" (in validate_custom_image, referencing
self.custom_image and the fetched Custom Image doc) so the document cannot be
saved/deployed with a failed image; if the warning-only behavior is intentional
instead, add a clear comment above validate_custom_image explaining why failed
images are permitted and update any tests or callers expecting a hard failure
accordingly.
- Around line 355-371: rebuild_custom_image_for_site sets the Frappe Site status
to "Building" and manually commits before enqueueing the build, which can lead
to inconsistent state; remove the redundant frappe.db.set_value and
frappe.db.commit calls in rebuild_custom_image_for_site and rely on
custom_image.enqueue_build_custom_image() (which already calls
_update_status("Building") and _sync_status_to_frappe_sites()) or move the
status update into the enqueue/build workflow or a post-commit hook so the
"Building" status is only set after the transaction and enqueue succeed.

In @nano_press/public/css/main.css:
- Around line 10-21: The blinking animation defined by the utility animate-blink
and the @keyframes blink doesn’t respect user motion preferences; wrap or
override that animation inside a prefers-reduced-motion: reduce media query so
users who prefer reduced motion don’t get the infinite blink. Specifically, add
a prefers-reduced-motion rule that sets animation: none (or animation-duration:
0s / animation-iteration-count: 1) for .animate-blink (or the @utility
animate-blink) and ensure the @keyframes blink remains unchanged for users
without the preference.

In @nano_press/templates/components/core/summary.html:
- Around line 178-189: When init() detects resuming via URL params (isComplete
or isDeploying) ensure the Alpine store's installer.currentStep is set so the
summary view becomes visible; before calling resumeCompletedDeployment(siteName)
or resumeDeployment(siteName) set $store.installer.currentStep = 4 (or set it
inside those functions) so x-show="$store.installer.currentStep === 4" will
evaluate true; update either resumeCompletedDeployment() and resumeDeployment()
to explicitly set $store.installer.currentStep = 4 on resume or add that
assignment in init() right before invoking them.
- Around line 515-625: The resumeDeployment method creates a local pollInterval
that isn't tracked on the component and so never cleared on destroy; add a data
property pollIntervalId (e.g., pollIntervalId: null), assign this.pollIntervalId
= setInterval(...) inside resumeDeployment, replace any
clearInterval(pollInterval) calls with clearInterval(this.pollIntervalId) and
set this.pollIntervalId = null afterward, and implement a lifecycle cleanup
(beforeDestroy or unmounted, or a destroy method) that clears
this.pollIntervalId if set to prevent leaks and stray network calls.
🧹 Nitpick comments (1)
nano_press/nano_press/doctype/frappe_site/frappe_site.py (1)

183-189: Localize error message for consistency.

The error message at line 188 is not wrapped with _() for localization, unlike other error messages in this file (lines 95, 97, 100, 111-112). For consistency and i18n support, localize this message as well.

♻️ Proposed fix
 		if self.is_custom and self.custom_image:
 			custom_image = frappe.get_cached_doc("Custom Image", self.custom_image)
 			if custom_image.build_status != "Built":
 				return {
 					"status": 400,
-					"message": f"Custom Image is not ready (status: {custom_image.build_status}). Please wait for build to complete.",
+					"message": _("Custom Image is not ready (status: {0}). Please wait for build to complete.").format(
+						custom_image.build_status
+					),
 				}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6197375 and 02b7e5c.

📒 Files selected for processing (5)
  • nano_press/nano_press/doctype/custom_image/custom_image.py
  • nano_press/nano_press/doctype/frappe_site/frappe_site.json
  • nano_press/nano_press/doctype/frappe_site/frappe_site.py
  • nano_press/public/css/main.css
  • nano_press/templates/components/core/summary.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • nano_press/nano_press/doctype/frappe_site/frappe_site.json
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:56.591Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.
📚 Learning: 2026-01-04T15:17:56.591Z
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:56.591Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.

Applied to files:

  • nano_press/nano_press/doctype/custom_image/custom_image.py
🧬 Code graph analysis (1)
nano_press/nano_press/doctype/frappe_site/frappe_site.py (1)
nano_press/nano_press/doctype/custom_image/custom_image.py (1)
  • enqueue_build_custom_image (244-255)
🪛 Ruff (0.14.10)
nano_press/nano_press/doctype/custom_image/custom_image.py

168-168: Do not catch blind exception: Exception

(BLE001)


202-202: Do not catch blind exception: Exception

(BLE001)


267-267: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (10)
nano_press/nano_press/doctype/frappe_site/frappe_site.py (2)

92-101: LGTM: Server validation messages properly localized.

The error messages in validate_server() are now correctly wrapped with _() for internationalization and use .format() for safe string interpolation. This follows Frappe framework best practices.


57-79: Fix inconsistent validation and message visibility for Custom Image status.

The validate_custom_image() method only checks for "Failed" status but does not validate or block "Draft" status, while _check_custom_image_status() defensively handles Draft with frappe.msgprint(). This is inconsistent and ineffective: Draft status cannot be selected via the UI (link_filters restrict to "Built" or "Building"), but if Draft somehow reaches this code, the msgprint won't be visible in non-interactive contexts (API calls, background jobs).

Additionally, the manual commits at lines 67 and 72 lack justification comments despite the nosemgrep suppressions.

Recommendations:

  1. Add validation in validate_custom_image() to throw an error for Draft status, preventing site creation entirely instead of relying on post-insert msgprint
  2. Replace the Draft msgprint with a frappe.throw() that interrupts creation before the database is modified
  3. Add comment explaining why manual commits are necessary if they must remain (e.g., "Status must persist immediately to reflect build state in external systems")
nano_press/templates/components/core/summary.html (2)

441-462: LGTM: Rebuild flow properly implemented.

The rebuildCustomImage() function correctly calls the backend API, handles success/error responses with appropriate user feedback via toasts, and resumes deployment polling. The error handling is defensive and user-friendly.


464-493: LGTM: Simplified build function with clearer separation of concerns.

The buildCustomImage() function now focuses solely on initiating the build and returning the image name, delegating polling and status checks to the resumeDeployment() function. This improves code clarity and reduces duplication.

nano_press/nano_press/doctype/custom_image/custom_image.py (6)

19-29: LGTM: Comprehensive Docker tag sanitization.

The _set_image_tag() method now properly sanitizes image names by:

  • Removing invalid characters (only a-z0-9_.- allowed)
  • Collapsing repeated separators
  • Stripping leading . and - characters
  • Providing a safe fallback ("image") when sanitization results in an empty string

This addresses the Docker image tag validation requirements and prevents build failures from invalid tag names.


63-84: LGTM: App sorting correctly handles sparse configurations.

The _sort_apps_by_order() method now properly handles cases where some apps_config entries may lack an app_name by building an explicit order mapping and sorting by app names rather than positional indices. This prevents misalignment issues and ensures apps are ordered correctly.


86-91: LGTM: Reusing computed base64 value avoids redundancy.

The method now uses self.apps_json_base64 (computed in before_save) instead of regenerating it, which is more efficient and ensures consistency between what's saved and what's deployed.


205-241: LGTM: Status synchronization properly implemented.

The _sync_status_to_frappe_sites() method correctly:

  • Checks if status actually changed before querying
  • Filters sites that need updating
  • Maps build statuses to appropriate site statuses
  • Logs synchronization for debugging

This ensures dependent Frappe Sites stay in sync with their Custom Image build lifecycle.

(The manual commit at line 232 is addressed in a separate comment.)


271-332: LGTM: Proper validation and secure token flow implemented.

The create_and_build_custom_image() function now includes:

  • Server existence check (line 276) before attempting operations
  • Server status validation (lines 280-283) ensuring it's prepared
  • Required field validation for custom apps (lines 304-307)
  • Clear error messages with context

The is_public=1 setting at line 317 is correct per the secure token flow: private apps with tokens are pre-registered via register_custom_app() API, so this fallback only executes for public apps.

Based on learnings, the token handling architecture is correctly implemented.


257-268: LGTM: Clean API interfaces for preview and status.

Both preview_apps_json_for_form() and get_build_status() provide clean, well-structured API responses with appropriate error handling. The preview method safely returns errors without breaking the UI, and the status method provides all necessary build information.

Also applies to: 335-343

Comment on lines +53 to +61
def _build_repo_url(self, app_doc):
repo_url = (app_doc.repo_url or "").strip()

return json.dumps(sorted_apps, indent=2)
if not app_doc.is_public and app_doc.pat_token and repo_url.startswith("https://"):
url_parts = repo_url.replace("https://", "").split("/", 1)
if len(url_parts) == 2:
return f"https://{app_doc.pat_token}@{url_parts[0]}/{url_parts[1]}"

def set_image_tag(self) -> str:
"""Generate image tag using just the image name."""
clean_name = self.image_name.lower().replace(" ", "-")
self.image_tag = f"{clean_name}:latest"
return repo_url
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine the specific lines
if [ -f "nano_press/nano_press/doctype/custom_image/custom_image.py" ]; then
  echo "=== File exists. Checking lines 53-61 ==="
  sed -n '50,65p' nano_press/nano_press/doctype/custom_image/custom_image.py | cat -n
else
  echo "File not found at expected path. Searching for it..."
  find . -name "custom_image.py" -type f 2>/dev/null
fi

Repository: BuildWithHussain/nano_press

Length of output: 767


🏁 Script executed:

# Check if commit f048dd1 exists in the repository
git log --oneline | head -20
echo "---"
git show f048dd1 --stat 2>/dev/null || echo "Commit f048dd1 not found in history"

Repository: BuildWithHussain/nano_press

Length of output: 1170


🏁 Script executed:

# Search for any references to this security issue or the _build_repo_url method
rg "_build_repo_url" --type py -A 10 -B 2

Repository: BuildWithHussain/nano_press

Length of output: 2317


🏁 Script executed:

# Check git history for changes to custom_image.py
git log --oneline nano_press/nano_press/doctype/custom_image/custom_image.py 2>/dev/null | head -20

Repository: BuildWithHussain/nano_press

Length of output: 168


🏁 Script executed:

# Show the actual diff for the custom_image.py changes in commit f048dd1
git show f048dd1 -- nano_press/nano_press/doctype/custom_image/custom_image.py

Repository: BuildWithHussain/nano_press

Length of output: 1775


🏁 Script executed:

# Check when _build_repo_url is called and how the URL is used
rg "_build_repo_url|_get_docker_build_config" --type py -A 5 -B 2

Repository: BuildWithHussain/nano_press

Length of output: 1482


🏁 Script executed:

# Look for any environment variable or secure credential passing mechanisms
rg "pat_token|PAT|token" nano_press/nano_press/doctype/custom_image/custom_image.py -B 3 -A 3

Repository: BuildWithHussain/nano_press

Length of output: 421


🏁 Script executed:

# Check if there are any existing Custom Apps with is_public=0 that would be vulnerable
rg "is_public.*0|is_public.*False" nano_press/nano_press/doctype/ --type py -B 3 -A 3

Repository: BuildWithHussain/nano_press

Length of output: 447


🏁 Script executed:

# Look for any tests or documentation about the token handling
rg "pat_token|_build_repo_url" nano_press/ --type py --type json --type md -B 2 -A 2 | head -50

Repository: BuildWithHussain/nano_press

Length of output: 3219


Security: PAT tokens embedded in URLs for all private custom apps.

The _build_repo_url() method injects PAT tokens directly into repository URLs (line 59), exposing them to logs, process listings, and error messages. Critically, the validate() method in apps.py automatically forces is_public = 0 whenever a custom app has a pat_token, which systematically triggers the vulnerable code path. This means tokens are embedded in URLs for every private custom app by design.

The claim that this was "Addressed in commit f048dd1" is incorrect—that commit only changed the function signature in create_and_build_custom_image() but did not fix the underlying vulnerability.

Tokens can leak in:

  • Ansible playbook logs and build output
  • Git command process listings (ps aux)
  • Error messages and stack traces
  • System logs and monitoring tools

Replace URL-embedded authentication with secure alternatives: pass tokens as separate deployment variables, use Git credential helpers, or store them as environment variables in the Ansible playbook.

Comment on lines +115 to +118
def _update_status(self, status):
frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
frappe.db.commit() # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
self._sync_status_to_frappe_sites()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add inline justification for manual commits as required by linter.

The manual commits at lines 117 and 232 have nosemgrep comments but lack the inline justification that the linter requires. Per the semgrep rule and previous review comments, manual commits should include a comment on the same line explaining why they're necessary.

♻️ Proposed fix
 def _update_status(self, status):
 	frappe.db.set_value("Custom Image", self.name, "build_status", status, update_modified=False)
-	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
+	frappe.db.commit()  # Manual commit: status must be visible to UI immediately during background build  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
 	self._sync_status_to_frappe_sites()
 	for site in linked_sites:
 		if new_status != site.status:
 			frappe.db.set_value("Frappe Site", site.name, "status", new_status, update_modified=False)
 			frappe.logger().info(
 				f"Synced status for Frappe Site {site.name}: {site.status} → {new_status}"
 			)
 
-	frappe.db.commit()  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit
+	frappe.db.commit()  # Manual commit: sync status to sites during background build  # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit

Also applies to: 232-232

🤖 Prompt for AI Agents
In @nano_press/nano_press/doctype/custom_image/custom_image.py around lines 115
- 118, The manual commits flagged by semgrep are missing the required inline
justification; update both occurrences of frappe.db.commit() in this file (the
one inside Custom Image._update_status and the other frappe.db.commit call later
in the file) to keep the existing "nosemgrep:
frappe-semgrep-rules.rules.frappe-manual-commit" annotation and add a brief
same-line comment explaining why the manual commit is necessary (e.g., to ensure
the build_status change is persisted immediately before calling
_sync_status_to_frappe_sites or before subsequent cross-site operations), so the
comment sits on the same line as frappe.db.commit() and references the reason
for immediate DB persistence.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @nano_press/templates/components/core/server.html:
- Line 164: The template sets userServers via "user_servers | tojson" which can
render null and later cause a runtime error when calling
this.userServers.find(); update the userServers initialization to ensure a safe
empty-array fallback — either apply the Jinja default filter (e.g.,
default('[]')) to user_servers before tojson or append a JS fallback (e.g., ||
[]) after the tojson output so that userServers is always an array for
subsequent calls to this.userServers.find().
🧹 Nitpick comments (1)
nano_press/templates/components/core/server.html (1)

271-321: Consolidate duplicate toast notifications.

The current flow shows multiple success toasts for a single operation:

  1. ping_server shows "Server connection verified successfully" (line 313)
  2. create_server_doc shows "Server record created successfully" (line 285)
  3. nextStep shows "Server verified and created successfully" (line 248-250)

This results in three success toasts for one user action, which can be confusing. Consider removing the toast notifications from ping_server and create_server_doc, keeping only the consolidated message in nextStep.

♻️ Proposed refactor
 async function create_server_doc(server_ip, ssh_user, ssh_port) {
     const url = '/api/resource/Server';
     try {
         const payload = {
             server_ip,
             ssh_user,
             ssh_port,
             traefik_email: '{{ user_email }}'
         };
 
         const response = await frappe_call(url, payload, "POST");
 
         if (response?.data?.name) {
-            const message = ("Server record created successfully");
-            toast(message, { type: "success" });
             return response.data;
         } else {
             throw new Error("Failed to create server record.");
         }
     } catch (err) {
         const message = err.message || ("Error creating server record");
         toast(message, { type: "danger" });
         throw err;
     }
 }
 
 async function ping_server(server_ip, ssh_user = "root", ssh_port = 22) {
     const url = '/api/v2/method/nano_press.api.ping_server';
     try {
         const payload = {
             host: server_ip,
             user: ssh_user,
             port: ssh_port
         };
 
         const response = await frappe_call(url, payload, "POST");
 
         if (!response?.data?.ok) {
             throw new Error(response?.data?.message || "Server is unreachable.");
         }
 
-        const message = ("Server connection verified successfully");
-        toast(message, { type: "success" });
-
         return response.data;
     } catch (err) {
         const message = err.message || ("Failed to verify server connection");
         toast(message, { type: "danger" });
         throw err;
     }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02b7e5c and 2d5ef1f.

📒 Files selected for processing (1)
  • nano_press/templates/components/core/server.html
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: Venkateshvenki404224
Repo: BuildWithHussain/nano_press PR: 22
File: nano_press/nano_press/doctype/custom_image/custom_image.py:246-257
Timestamp: 2026-01-04T15:17:56.591Z
Learning: In Nano Press's secure token flow, private app tokens are registered via the `register_custom_app()` API before `create_and_build_custom_image()` is called. The `custom_apps` payload passed to `create_and_build_custom_image()` does NOT contain tokens (only name, githubUrl, branch, app_reference_id). Apps with tokens already exist in the database when `create_and_build_custom_image()` runs, so the fallback doc creation (when `frappe.db.exists("Apps", app_name)` is False) only applies to public apps and correctly sets `is_public=1`.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Server
🔇 Additional comments (5)
nano_press/templates/components/core/server.html (5)

14-59: LGTM! Server selection UI integrates cleanly.

The conditional layout and server selection dropdown are well-implemented. The manual IP input correctly resets selectedServerName and verification state, preventing inconsistencies between selected and manually entered servers.


166-189: LGTM! Server selection logic is well-structured.

The method correctly populates form fields from the selected server, handles verification state appropriately, and provides user feedback via toast notifications. The String conversion for ssh_port at line 177 ensures compatibility with the input field's text model.


196-224: LGTM! Data clearing is comprehensive and user-friendly.

The confirmation dialog, comprehensive localStorage key list, and delayed reload provide a good user experience. The toast notification gives clear feedback before the page reloads.


226-267: LGTM! Step navigation logic is correct.

The function properly handles both verified and unverified states, validates IP addresses, and stores the server name from the API response at line 254. Error handling and loading state management are appropriate.


323-327: LGTM! Clean composition of verification steps.

The function appropriately sequences the ping and create operations, with error propagation handled by the underlying functions.

Cleaned up several code quality issues identified during review to improve reliability and user experience.

Fixes:
- Remove duplicate status sync call in custom image build failure handler
- Fix polling interval memory leak by tracking and cleaning up on component destroy
- Ensure summary component visible when resuming from URL parameters
- Add null safety fallback for user servers array to prevent runtime errors
- Remove redundant status update in rebuild endpoint (already handled by sync)
- Upgrade failed image validation from warning to hard block for data integrity
- Add accessibility support for users with reduced motion preferences
@Venkateshvenki404224
Copy link
Collaborator

Venkateshvenki404224 commented Jan 7, 2026

image image image image

Now the Summery Page is refresh proof ! @Rl0007

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants