Conversation
Add public landing page and PenguinUI sidebar navigation. Still needs browser cache clearing and layout fixes. Partial implementation of #19
- 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.
- 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
…ining the frappe versions
…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
- 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
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.
… 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.
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
- Remove nested x-data component and merge copy functionality into main serverSetup() - Consolidate duplicate IP validation regex into validateIP() method - Remove unused 'copied' property - Remove redundant 'typeof toast !== undefined' checks (toast is always available) - Move global helper functions into serverSetup() as methods (createServerDoc, pingServer, verifyAndCreateServer) - Replace string concatenation with template literals - Remove unnecessary parentheses around string literals - Fix indentation throughout serverSetup() function for better readability This refactoring eliminates code duplication, removes dead code, prevents global namespace pollution, and improves overall maintainability without changing functionality.
- Remove redundant 'typeof toast !== undefined' checks (4 instances) - Fix indentation throughout script section after linter corruption - Improve code readability and consistency
- Remove redundant 'typeof toast !== undefined' check - Remove redundant local prevStep() method (use inherited from parent) - Move verify_domain() global function into domainSetup() as verifyDomain() method - Remove empty init() function and x-init directive from HTML - Replace hardcoded step 4 navigation with increment operator for flexibility
summary.html: - Remove redundant 'typeof toast !== undefined' checks (19 instances) - Remove unused buildingImage property and assignments - Remove redundant local prevStep() method (use inherited from parent) self-host.html: - Remove massive dead code from frappeInstaller() parent component - Delete unused properties: showAdvanced, generatedSSHKey, showCustomAppDialog, customAppForm, domain, domainVerified, domainStatusMessage, isLoading, deploying, deployComplete, deployStep, deploymentStatus, copied - Delete unused methods: copyToClipboard(), toggleApp(), getSelectedAppsNames(), startDeployment(), visitSite() - These were legacy code from old implementation before child components were properly isolated - Parent now only contains: stepNames, store getters, nextStep(), prevStep() This refactoring eliminates ~100 lines of legacy dead code that was creating confusion and making the codebase harder to maintain. All functionality has been properly moved to child components where it belongs.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughThis PR introduces custom app management APIs, a new App Version DocType, significantly refactors the Custom Image build pipeline with modular helpers, modernizes the UI using component-based architecture with landing pages, adds internationalization support, and updates deployment orchestration flows across backend and frontend. Changes
Sequence DiagramssequenceDiagram
actor User
participant Client as Client / Browser
participant API as API Endpoint
participant DB as Database (Apps)
User->>Client: Fill custom app form
Client->>API: POST /api/method/register_custom_app<br/>(app_name, github_url, branch, token)
API->>DB: Check if Apps doc exists
alt App exists
API->>DB: Update repo_url, branch, pat_token, order
API->>DB: Save with ignore_permissions
else App not found
API->>DB: Create new Apps doc<br/>(is_custom=1, enabled=1, order=999)
API->>DB: Insert with ignore_permissions
end
API->>DB: Commit transaction
API->>Client: Return success + app_reference_id
Client->>Client: Show success toast
Client->>Client: Add app to localCustomApps
Client->>Client: Reset form & close modal
sequenceDiagram
actor User
participant Client as Client / Browser
participant API as API Endpoint
participant AnsibleAPI as Ansible Runner
participant Server as Target Server
participant Docker as Docker Registry
User->>Client: Trigger custom image build
Client->>API: Call enqueue_build_custom_image()
API->>API: Set build_status to Building
API->>API: Queue async build task
API->>Client: Return queued status
par Build Process
API->>AnsibleAPI: Run build_custom_image.yml
AnsibleAPI->>Server: SSH connection established
Server->>Server: Clone/update frappe_docker<br/>(fresh clone)
Server->>Server: Build custom image<br/>(with apps.json)
Server->>Docker: Push image to registry
end
alt Build Success
API->>API: Set build_status to Built
API->>API: Update built_at timestamp
API->>API: Sync status to linked FrappeSite
API->>Client: Send success notification
else Build Failure
API->>API: Set build_status to Failed
API->>API: Log error details
API->>Client: Send error notification
end
sequenceDiagram
actor User
participant Client as Client / Browser
participant API as API Endpoint
participant CIImage as Custom Image
participant FrappeSite as Frappe Site
participant Ansible as Ansible Runner
participant Server as Server
User->>Client: Review summary & click Deploy
Client->>API: startDeployment()
API->>API: Call callPrepareServer()
API->>Server: Prepare server environment
alt Custom Image present
API->>CIImage: Check build_status
API->>API: Call buildCustomImage()
API->>Ansible: Queue image build
Ansible->>Server: Build & push image
API->>API: Poll build status
end
API->>FrappeSite: Create Frappe Site doc
API->>API: Call callPrepareForDeployment()
API->>Ansible: Provision site resources
API->>API: Call callDeploySite()
API->>Ansible: Execute deployment playbook
Ansible->>Server: Deploy application
API->>API: Poll for deployment completion
alt Deployment Complete
API->>FrappeSite: Fetch credentials
API->>Client: Display site URL & credentials
else Deployment Failed
API->>Client: Show error & status
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
nano_press/api.py (1)
16-17: Logic error in validation condition.The condition
if not host and not user and not portonly throws when ALL parameters are missing. If only one or two are provided, the validation passes butrun_pingwill likely fail or behave unexpectedly.🐛 Proposed fix
- if not host and not user and not port: + if not host or not user or not port: frappe.throw(_("Missing required connection details: provide 'host', 'user', and 'port'"))nano_press/templates/components/core/server.html (1)
130-132: Malformed SVG path data.The SVG path contains
0 714 12H0which appears to be a typo - likely should be0 7 14 12H0(missing space). This could cause the spinner to render incorrectly.🐛 Proposed fix
<path class="opacity-75" fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> </path>nano_press/nano_press/doctype/frappe_site/frappe_site.json (2)
53-61: Update the Python type annotation infrappe_site.pyto include the new status values.The JSON options include "Building" and "Build Failed", and the code actively sets and checks for these statuses (lines 65-71 in frappe_site.py, custom_image.py line 213, and summary.html), but the Python type annotation at line 40 is incomplete:
status: DF.Literal["Not Deployed", "Ready To Deploy", "Deploying", "Deployed", "Failed", "Stopped"]Should include:
status: DF.Literal["Not Deployed", "Building", "Build Failed", "Ready To Deploy", "Deploying", "Deployed", "Failed", "Stopped"]
99-106: Deployment is guarded, but UI link filters should be restricted.Deployment is protected by
prepare_for_deployment()which returns a 400 error ifbuild_status != "Built". However, the link_filters in the JSON allow selecting images with status "Building", which users can then save. To prevent this UX confusion, restrict the link_filters to only"Built"status, or add validation invalidate_custom_image()to reject "Building" images during save.
🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 48-51: The CI uses actions/setup-python@v4 with python-version:
'3.14' which lacks proper Python 3.14 support; update the action reference to
actions/setup-python@v5 (or later) and keep python-version at '3.14', and before
merging verify your bench/apps are running Frappe/ERPNext v16+ (and upgrade any
apps) and run full dependency tests to catch third‑party package
incompatibilities with Python 3.14.
In `@nano_press/hooks.py`:
- Around line 155-161: Removing the scheduler_events block disables the periodic
call to ping_server which updates verify_status and last_verified_at, causing
stale verification state; restore automatic refresh by re-adding the
scheduler_events cron entry or gating it behind a config flag (e.g.,
ENABLE_PING_SCHEDULER) so ping_server runs every 5 minutes, or implement an
alternative refresh mechanism (e.g., trigger ping_server in relevant workflows
like create_site/build_image/remote_build) and ensure code paths that check
verify_status (and read last_verified_at) call the refresh when necessary.
In `@nano_press/nano_press/doctype/custom_image/custom_image.py`:
- Around line 205-232: The check using self.has_value_changed("build_status") in
_sync_status_to_frappe_sites can miss updates because _update_status uses
frappe.db.set_value (which mutates the DB but not the in-memory document);
modify _sync_status_to_frappe_sites to accept either a force boolean or the
new_status value (e.g., def _sync_status_to_frappe_sites(new_status=None,
force=False)), then if new_status is provided use it directly (or skip the
has_value_changed check when force=True), update callers (notably
_update_status) to pass the new status or force=True after calling
frappe.db.set_value, and remove reliance on has_value_changed alone so the site
sync always runs when the DB was updated via frappe.db.set_value.
In `@nano_press/nano_press/doctype/frappe_site/frappe_site.py`:
- Around line 59-78: The status values set by _check_custom_image_status
("Building" and "Build Failed") are missing from the DF.Literal type for the
status field; update the status field's DF.Literal definition (the "status"
field on the Frappe Site doctype) to include "Building" and "Build Failed"
alongside the existing values so form validation and rendering match the values
written by _check_custom_image_status.
In `@nano_press/nano_press/utils/ansible/playbooks/build_custom_image.yml`:
- Around line 32-41: The tasks that unconditionally remove and re-clone the
frappe_docker repo ("Remove frappe_docker directory to ensure clean clone" and
"Clone frappe_docker repo (fresh)") should be executed only when a build is
actually required; compute a boolean like build_needed (e.g., set_fact:
build_needed: "{{ not image_exists or force_build }}") or evaluate the existing
image existence check earlier, move the image existence check task before the
cleanup/clone, and add a when: build_needed (or equivalent condition using
image_exists and force_build) to both the file removal and git clone tasks so
the repo is not wiped/cloned when image_exists is true and force_build is false.
In `@nano_press/public/js/script.js`:
- Around line 26-32: Update the client-side flow so customApps entries only
include name, githubUrl, and branch (remove token from customAppForm and any
place token is pushed into the customApps array) and add input validation inside
addCustomApp() to reject/flag empty or invalid name and githubUrl (e.g.,
non-empty name and a valid URL pattern) before pushing to the customApps array;
keep token handling separate and ensure tokens are sent only via the
register_custom_app() endpoint prior to calling create_and_build_custom_image().
In `@nano_press/public/js/vendor/alpinejs/plugins/mask.js`:
- Line 1: The vendored minified Alpine mask plugin (the IIFE that defines
function b, helper functions m/x/w, and registers via
document.addEventListener("alpine:init" -> window.Alpine.plugin(b)) lacks
source/version/attribution; add proper third-party attribution by (1) adding a
header comment to this file that states the plugin name, exact version or commit
hash, original source URL, and the upstream license text/summary, and (2)
creating/updating THIRD_PARTY_NOTICES or NOTICE to include the same information
and the full original license (not the template license.txt), including
copyright holder and license SPDX identifier, so consumers can trace the origin
and license of the mask plugin.
In `@nano_press/templates/components/core/stepper.html`:
- Line 4: The progress bar div currently has no width binding so it never grows;
update the div (the element with classes "absolute top-4 left-0 h-px bg-gray-900
z-0 transition-all duration-500") to set its width dynamically from currentStep
by binding style.width to (currentStep - 1) * 33.33% (use the template's binding
syntax, e.g., :style or x-bind:style) so the bar reflects progress and still
transitions.
In `@nano_press/templates/components/core/summary.html`:
- Around line 293-300: In startDeployment's catch block you need to stop the
polling started by resumeDeployment to avoid a zombie loop: call
clearInterval(this.pollingInterval) (or clearInterval(this.deploymentInterval)
if that's the name used) and set that field to null/undefined before/when
setting this.deploying = false and this.deploymentStatus; update the catch block
in startDeployment to perform this cleanup so any existing interval is cleared
and won't continue polling after a failure.
In `@nano_press/templates/components/landing/features.html`:
- Line 13: The Tailwind gradient color utilities on the container div with 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" are
ineffective because there's no gradient direction; update that class to include
a gradient direction such as "bg-gradient-to-br" (or another desired direction)
so it becomes "bg-gradient-to-br from-gray-50 to-white …", and apply the same
change to the other two similar divs that use "from-gray-50 to-white" (the
blocks at the other occurrences).
In `@nano_press/templates/components/landing/hero.html`:
- Around line 25-32: The external anchor element that renders the "View on
GitHub" button (the <a> with
href="https://github.com/BuildWithHussain/nano_press" and target="_blank")
should include rel="noopener noreferrer" to mitigate tab-napping; update that
<a> tag to add rel="noopener noreferrer" alongside the existing target
attribute.
In `@nano_press/www/index.html`:
- Around line 24-26: The external GitHub link anchor element that uses
target="_blank" (the <a> with
href="https://github.com/BuildWithHussain/nano_press") is missing the rel
attribute; update that anchor to include rel="noopener noreferrer" to prevent
tabnabbing and ensure the new tab cannot access the opener window.
In `@nano_press/www/self_host.py`:
- Around line 32-36: The get_frappe_versions function uses frappe.db.get_all
which bypasses permissions; change it to use frappe.get_list so reads respect
user permissions: replace the call to frappe.db.get_all("App Version",
fields=[...], order_by="creation asc") with frappe.get_list("App Version",
fields=[...], order_by="creation asc") and ensure you do not pass
ignore_permissions=True so permission checks remain enforced (keep the function
name get_frappe_versions and the same fields/order_by parameters).
🧹 Nitpick comments (14)
nano_press/nano_press/utils/remote_builder.py (2)
62-66: Inconsistent i18n: This error message is not wrapped for translation.The error message at line 65-66 is not wrapped with
_(), unlike the other validation messages in this file. For consistent localization, consider wrapping this message as well.♻️ Suggested fix
def _validate_server(self, server_doc) -> None: if server_doc.verify_status != "Verified": frappe.throw( - f"Server {server_doc.name} is not verified. Please verify server connectivity first." + _("Server {0} is not verified. Please verify server connectivity first.").format(server_doc.name) )
210-221: Inconsistent i18n: Error messages intrigger_remote_buildnot translated.These error messages returned in the dict are not wrapped with
_()for translation, unlike the similar validation messages in the class methods above.♻️ Suggested fix
if not server_name: if not custom_image_doc.server_name: return { "success": False, - "error": "No server linked to Custom Image and no server_name provided", + "error": _("No server linked to Custom Image and no server_name provided"), } server_name = custom_image_doc.server_name server_doc = frappe.get_doc("Server", server_name) if server_doc.verify_status != "Verified": return { "success": False, - "error": f"Server {server_doc.name} is not verified. Please verify server connectivity first.", + "error": _("Server {0} is not verified. Please verify server connectivity first.").format(server_doc.name), }nano_press/utils/ansible_runner.py (2)
60-62: Inconsistent i18n: This error message is not wrapped for translation.For consistency with the other translated messages in this file, consider wrapping this error message as well.
♻️ Suggested fix
server_docname = frappe.db.get_value("Server", {"server_ip": host}, "name") if not server_docname: - frappe.throw(f"No Server document found with server_ip={host}") + frappe.throw(_("No Server document found with server_ip={0}").format(host))
120-122: Inconsistent i18n: Same untranslated error pattern.Similar to line 62, this error message should also be wrapped with
_()for consistent localization.♻️ Suggested fix
server_docname = frappe.db.get_value("Server", {"server_ip": host}, "name") if not server_docname: - frappe.throw(f"No Server document found with server_ip={host}") + frappe.throw(_("No Server document found with server_ip={0}").format(host))nano_press/nano_press/utils/ansible/src/AnsibleRunner.py (1)
259-260: Inconsistent i18n: This error message is not wrapped for translation.For consistency with the other translated messages in this class, consider wrapping this error as well.
♻️ Suggested fix
if not row: - frappe.throw(f"Server not found for filter: {filters}") + frappe.throw(_("Server not found for filter: {0}").format(filters))nano_press/templates/components/landing/features.html (1)
82-82: Minor text inconsistency: "Real time" should be "Real-time"."Real-time" is typically hyphenated when used as a compound adjective.
📝 Suggested fix
- <h4 class="text-lg font-semibold text-gray-900 mb-2">Real time Logs</h4> + <h4 class="text-lg font-semibold text-gray-900 mb-2">Real-time Logs</h4>nano_press/api.py (1)
96-104: Consider settingis_custom=1when updating an existing app.When an existing Apps document is updated,
is_customis not set. If the app was previously created as a non-custom app, this could lead to inconsistencies where the app is treated as custom in some contexts but not others.♻️ Proposed fix
if frappe.db.exists("Apps", app_name_normalized): app_doc = frappe.get_doc("Apps", app_name_normalized) app_doc.repo_url = github_url app_doc.branch = branch + app_doc.is_custom = 1 if token: app_doc.pat_token = token if order is not None: app_doc.order = order app_doc.save(ignore_permissions=True)nano_press/templates/components/core/domain.html (1)
106-114: Potential double-click race during step advancement delay.After successful verification,
isLoadingis set tofalsein thefinallyblock immediately, but the step advancement happens after a 1000mssetTimeout. During this window, a user could click the button again, potentially triggering another verification or step advancement.♻️ Proposed fix - keep loading state until step advances
await this.verifyDomain(domain, expectedIp); this.$store.installer.domainVerified = true; setTimeout(() => { this.$store.installer.currentStep++; + this.isLoading = false; }, 1000); } catch (err) { console.error('❌ Domain verification failed:', err); this.$store.installer.domainVerified = false; - } finally { this.isLoading = false; }nano_press/templates/components/core/server.html (2)
157-163: Missing error handling for clipboard API.
navigator.clipboard.writeText()returns a Promise that can reject (e.g., permission denied, insecure context). The current implementation doesn't handle failures, which could leave the UI in an inconsistent state.♻️ Proposed fix
copyToClipboard() { - navigator.clipboard.writeText(this.copyText); - this.copyNotification = true; - setTimeout(() => { - this.copyNotification = false; - }, 3000); + navigator.clipboard.writeText(this.copyText) + .then(() => { + this.copyNotification = true; + setTimeout(() => { + this.copyNotification = false; + }, 3000); + }) + .catch(err => { + toast('Failed to copy to clipboard', { type: 'danger' }); + console.error('Clipboard error:', err); + }); },
273-276: Consider consolidating success toasts inverifyAndCreateServer.Currently, both
pingServerandcreateServerDocshow individual success toasts. When called viaverifyAndCreateServer, users see two success toasts, and ifcreateServerDocfails afterpingServersucceeds, the ping success toast is misleading. Consider suppressing intermediate toasts when called fromverifyAndCreateServer.nano_press/nano_press/doctype/frappe_site/frappe_site.py (2)
180-187: Inconsistent use of translation wrapper.The error message on line 186 uses an f-string directly without the
_()translation wrapper, while other error messages in this file (lines 95-100, 110-113) are properly translated.Proposed fix
- return { - "status": 400, - "message": f"Custom Image is not ready (status: {custom_image.build_status}). Please wait for build to complete.", - } + return { + "status": 400, + "message": _("Custom Image is not ready (status: {0}). Please wait for build to complete.").format(custom_image.build_status), + }
353-366: Inconsistent error message formatting.Line 356 uses an f-string for the error message, but
_()is used elsewhere in this file for user-facing messages. Additionally, the function returns{"status": "error", ...}on line 361 but{"status": "success", ...}on line 366—consider using HTTP-style numeric status codes (like the rest of the file) for consistency.Proposed fix for translation consistency
`@frappe.whitelist`() def rebuild_custom_image_for_site(site_name: str) -> dict: if not frappe.db.exists("Frappe Site", site_name): - frappe.throw(f"Frappe Site {site_name} not found") + frappe.throw(_("Frappe Site '{0}' not found").format(site_name))nano_press/templates/components/core/summary.html (1)
208-213:destroy()method may not be invoked by Alpine.The
destroy()method clears the polling interval, but Alpine.js doesn't automatically call adestroy()method. You should use Alpine'sx-effectwith cleanup or listen for the@destroyevent to ensure the interval is cleared when the component unmounts.Consider using Alpine's lifecycle:
init() { + this.$watch('$store.installer.currentStep', (value) => { + if (value !== 4 && this.pollIntervalId) { + clearInterval(this.pollIntervalId); + this.pollIntervalId = null; + } + }); const params = new URLSearchParams(window.location.search);Or add
x-on:removeto the element:<div x-show="..." x-data="summarySetup()" x-init="init()" `@remove`="destroy()">nano_press/nano_press/doctype/custom_image/custom_image.py (1)
168-169: Consider logging exception details for notification failures.Static analysis flags catching bare
Exception(BLE001). While catching all exceptions is appropriate here to prevent notification failures from breaking the build flow, the current logging doesn't capture the actual error details.Proposed fix to capture exception details
- except Exception: - frappe.log_error(title=_("Build Notification Failed")) + except Exception as e: + frappe.log_error(message=str(e), title=_("Build Notification Failed"))- except Exception: - frappe.log_error(title=_("Email Notification Failed")) + except Exception as e: + frappe.log_error(message=str(e), title=_("Email Notification Failed"))Also applies to: 202-203
| - name: Setup Python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.10' | ||
| python-version: '3.14' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Python 3.14 release status January 2026
💡 Result:
Status as of January 22, 2026:
- Python 3.14 (final) was released October 7, 2025. [1][2]
- Bugfix releases 3.14.1 and 3.14.2 were published in December 2025. [1][3]
- The 3.14 release schedule lists the next expected bugfix (3.14.3) for February 3, 2026. [2]
Sources: official Python release page and PEP 745 (release schedule). [1] [2] [3]
🌐 Web query:
actions/setup-python v4 v5 Python 3.14 support compatibility
💡 Result:
Short answer: use v5 (or later). actions/setup-python@v5 supports installing Python 3.14 (including free‑threaded builds / prereleases when needed) via the python-version input; v4 is older and may not handle newer Python releases/tooling the same way. See GitHub's setup-python docs and release notes for v5 and examples showing 3.14 usage. [1][2][3][4]
Relevant links:
- actions/setup-python releases (v4→v5 changelog). [1]
- setup-python README / Marketplace (v5+ usage examples). [2]
- GitHub Actions docs showing setup-python@v5 usage in Python matrices. [3]
- Community/example showing 3.14 and free‑threaded 3.14 usage with setup-python@v5 (use allow-prereleases for pre-release builds). [4]
If you want, I can paste the exact workflow snippet to set up Python 3.14 (regular or free‑threaded) with setup-python@v5.
🌐 Web query:
frappe-bench Python 3.14 compatibility
💡 Result:
Short answer: Frappe/erpnext support for Python 3.14 is tied to the app version — Frappe/ERPNext v16 (and develop) require Python 3.14+, but older branches (v15 and earlier) do not accept 3.14. If your bench/apps are still on v15 or older, Python 3.14 will fail. [1][2][4]
Details and practical notes:
- Official requirements list Frappe v16/develop as Python 3.14 (v14/v15 use 3.10+). [1]
- Users report installing v16 needs Python 3.14; trying to install frappe (older constraint) on 3.14 produced “requires a different Python: 3.14.x not in '<3.14,>=3.10'”. That indicates older frappe packages will reject 3.14. [2]
- Some third‑party deps (e.g., eventlet/socketio) may not yet be compatible with 3.14 — expect occasional dependency issues when moving to v16. [3]
- Recommendation: run Python 3.14 only for benches where all apps are upgraded to v16/develop; for existing v15 or custom apps stick to a supported Python (3.10/3.11/3.12) until you upgrade and verify third‑party deps. [1][2][3]
Sources: Frappe docs and community threads. [1][2][3][4]
Upgrade actions/setup-python to v5 and verify Frappe version compatibility with Python 3.14.
Python 3.14 (released October 2025) is stable as of January 2026. However:
actions/setup-python@v4does not support Python 3.14 properly—upgrade to@v5(or later), which explicitly supports Python 3.14.- Only Frappe/ERPNext v16+ support Python 3.14. If your bench runs v15 or earlier, Python 3.14 will fail at install time. Verify that all apps in this bench are upgraded to v16+ before using Python 3.14.
- Some third-party dependencies may not yet be fully compatible with Python 3.14—monitor for dependency conflicts during testing.
🧰 Tools
🪛 actionlint (1.7.10)
49-49: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🤖 Prompt for AI Agents
In @.github/workflows/ci.yml around lines 48 - 51, The CI uses
actions/setup-python@v4 with python-version: '3.14' which lacks proper Python
3.14 support; update the action reference to actions/setup-python@v5 (or later)
and keep python-version at '3.14', and before merging verify your bench/apps are
running Frappe/ERPNext v16+ (and upgrade any apps) and run full dependency tests
to catch third‑party package incompatibilities with Python 3.14.
| # scheduler_events = { | ||
| # "cron": { | ||
| # "*/5 * * * *": [ | ||
| # "nano_press.utils.ansible_runner.ping_server", | ||
| # ], | ||
| # } | ||
| # } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file and understand the context
find . -name "hooks.py" -type f | head -20Repository: BuildWithHussain/nano_press
Length of output: 92
🏁 Script executed:
# Search for the ping_server function to understand what it does
rg "def ping_server" -A 20Repository: BuildWithHussain/nano_press
Length of output: 2336
🏁 Script executed:
# Search for references to ping_server in the codebase
rg "ping_server" --type pyRepository: BuildWithHussain/nano_press
Length of output: 352
🏁 Script executed:
# Search for scheduler_events usage
rg "scheduler_events" --type pyRepository: BuildWithHussain/nano_press
Length of output: 113
🏁 Script executed:
# Search for health/status update mechanisms
rg "health|status|verify" --type py | grep -i "server\|scheduler" | head -30Repository: BuildWithHussain/nano_press
Length of output: 1607
🏁 Script executed:
# Search for where verify_status is modified/updated
rg "verify_status\s*=" --type py -B 2 -A 2Repository: BuildWithHussain/nano_press
Length of output: 467
🏁 Script executed:
# Check the Server doctype definition to understand verify_status
find . -path "*doctype/server*" -type f | head -20Repository: BuildWithHussain/nano_press
Length of output: 324
🏁 Script executed:
# Look at the full Server.py file to understand verify_status lifecycle
cat -n nano_press/nano_press/doctype/server/server.py | head -100Repository: BuildWithHussain/nano_press
Length of output: 3190
🏁 Script executed:
# Get the full Server.py file to understand all status changes
wc -l nano_press/nano_press/doctype/server/server.pyRepository: BuildWithHussain/nano_press
Length of output: 121
🏁 Script executed:
# Read the rest of the Server.py file
cat -n nano_press/nano_press/doctype/server/server.py | tail -200Repository: BuildWithHussain/nano_press
Length of output: 8056
🏁 Script executed:
# Search for all methods/functions that change verify_status or check it
ast-grep --pattern 'def $_($_) {
$$$
verify_status
$$$
}' --type pyRepository: BuildWithHussain/nano_press
Length of output: 250
🏁 Script executed:
# More targeted search for all verify-related methods
rg "def.*verify|verify_status|prepare" nano_press/nano_press/doctype/server/server.py -B 2 -A 10Repository: BuildWithHussain/nano_press
Length of output: 4522
🏁 Script executed:
# Check if ping_server actually updates the database/verify_status
rg "ping_server" -A 30 nano_press/utils/ansible_runner.py | grep -A 30 "def ping_server"Repository: BuildWithHussain/nano_press
Length of output: 880
🏁 Script executed:
# Look for any database/status updates in ping_server
rg "ping_server" -A 50 nano_press/api.py | head -80Repository: BuildWithHussain/nano_press
Length of output: 1309
🏁 Script executed:
# Check if there are other places where verify_status might be degraded or reset
rg 'verify_status\s*=' nano_press/nano_press/doctype/server/ -B 3 -A 3Repository: BuildWithHussain/nano_press
Length of output: 1819
🏁 Script executed:
# See if there's a separate verify method that might call ping_server and update status
rg "def.*verify" nano_press/nano_press/doctype/server/server.py -B 2 -A 20Repository: BuildWithHussain/nano_press
Length of output: 53
🏁 Script executed:
# Get the full ping_server function from ansible_runner.py
rg "def ping_server" -A 60 nano_press/utils/ansible_runner.pyRepository: BuildWithHussain/nano_press
Length of output: 1642
🏁 Script executed:
# Check if verify_status can be changed back from "Prepared" to something else
rg "verify_status.*=" nano_press/ -r --type py -B 2 -A 1Repository: BuildWithHussain/nano_press
Length of output: 3349
🏁 Script executed:
# Look at the git history to understand why the scheduler was commented out
git log --oneline -20 nano_press/hooks.pyRepository: BuildWithHussain/nano_press
Length of output: 151
🏁 Script executed:
# Check if there are any git diffs or comments about the scheduler removal
git log -p --all -S "scheduler_events" -- nano_press/hooks.py | head -100Repository: BuildWithHussain/nano_press
Length of output: 53
Scheduler removal will cause verify_status to become stale without automatic refresh.
The commented-out scheduler_events was responsible for running ping_server every 5 minutes. The ping_server function actively updates the database with verify_status ("Verified" or "Failed") and last_verified_at. Without this scheduler, verify_status will no longer be automatically refreshed—if a server becomes unreachable, the status remains stale until manual verification. Since critical operations (creating sites, building images, remote builds) check verify_status before proceeding, users could silently fail with outdated status information. If this is intentional, guard the scheduler with a config flag or provide an alternative mechanism to keep status fresh.
🤖 Prompt for AI Agents
In `@nano_press/hooks.py` around lines 155 - 161, Removing the scheduler_events
block disables the periodic call to ping_server which updates verify_status and
last_verified_at, causing stale verification state; restore automatic refresh by
re-adding the scheduler_events cron entry or gating it behind a config flag
(e.g., ENABLE_PING_SCHEDULER) so ping_server runs every 5 minutes, or implement
an alternative refresh mechanism (e.g., trigger ping_server in relevant
workflows like create_site/build_image/remote_build) and ensure code paths that
check verify_status (and read last_verified_at) call the refresh when necessary.
| def _sync_status_to_frappe_sites(self): | ||
| if not self.has_value_changed("build_status"): | ||
| return | ||
|
|
||
| linked_sites = frappe.get_all( | ||
| "Frappe Site", | ||
| filters={ | ||
| "custom_image": self.name, | ||
| "status": ["in", ["Not Deployed", "Building", "Build Failed"]], | ||
| }, | ||
| fields=["name", "status", "owner"], | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| frappe.log_error(f"Failed to send email notification: {e!s}", "Email Notification") | ||
|
|
||
| if not linked_sites: | ||
| return | ||
|
|
||
| @frappe.whitelist() | ||
| def preview_apps_json(custom_image_name: str) -> dict[str, Any]: | ||
| """API endpoint to preview generated apps.json for a Custom Image. | ||
| new_status = self._map_build_status_to_site_status() | ||
| if not new_status: | ||
| return | ||
|
|
||
| Args: | ||
| custom_image_name: Name of the Custom Image document | ||
| 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}" | ||
| ) | ||
|
|
||
| Returns: | ||
| Dict with apps_json content and base64 version | ||
| """ | ||
| try: | ||
| custom_image = frappe.get_cached_doc("Custom Image", custom_image_name) | ||
| apps_json = custom_image.generate_apps_json() | ||
| apps_json_base64 = custom_image.generate_apps_json_base64() | ||
| frappe.db.commit() # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit |
There was a problem hiding this comment.
has_value_changed may not work as expected after db.set_value.
_sync_status_to_frappe_sites is called from _update_status (line 118) which uses frappe.db.set_value to update the status. However, has_value_changed on line 206 checks against the document's in-memory state, which may not reflect the database change made via db.set_value without a reload().
This could cause the sync to be skipped when it should run.
Proposed fix: Pass the new status explicitly or reload before checking
- def _sync_status_to_frappe_sites(self):
- if not self.has_value_changed("build_status"):
- return
+ def _sync_status_to_frappe_sites(self, force=False):
+ if not force and not self.has_value_changed("build_status"):
+ returnThen call with self._sync_status_to_frappe_sites(force=True) from _update_status.
🤖 Prompt for AI Agents
In `@nano_press/nano_press/doctype/custom_image/custom_image.py` around lines 205
- 232, The check using self.has_value_changed("build_status") in
_sync_status_to_frappe_sites can miss updates because _update_status uses
frappe.db.set_value (which mutates the DB but not the in-memory document);
modify _sync_status_to_frappe_sites to accept either a force boolean or the
new_status value (e.g., def _sync_status_to_frappe_sites(new_status=None,
force=False)), then if new_status is provided use it directly (or skip the
has_value_changed check when force=True), update callers (notably
_update_status) to pass the new status or force=True after calling
frappe.db.set_value, and remove reliance on has_value_changed alone so the site
sync always runs when the DB was updated via frappe.db.set_value.
| def _check_custom_image_status(self): | ||
| if not self.is_custom or not self.custom_image: | ||
| return | ||
|
|
||
| custom_image = frappe.get_cached_doc("Custom Image", self.custom_image) | ||
|
|
||
| if custom_image.build_status == "Building": | ||
| frappe.db.set_value("Frappe Site", self.name, "status", "Building", update_modified=False) | ||
| frappe.db.commit() # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit | ||
| elif custom_image.build_status == "Built": | ||
| pass | ||
| elif custom_image.build_status == "Failed": | ||
| frappe.db.set_value("Frappe Site", self.name, "status", "Build Failed", update_modified=False) | ||
| frappe.db.commit() # nosemgrep: frappe-semgrep-rules.rules.frappe-manual-commit | ||
| elif custom_image.build_status == "Draft": | ||
| frappe.msgprint( | ||
| _("Custom Image has not been built yet. Please build it first."), | ||
| indicator="orange", | ||
| alert=True, | ||
| ) |
There was a problem hiding this comment.
Status values not in type definition.
The _check_custom_image_status method sets status values "Building" (line 66) and "Build Failed" (line 71), but these are not included in the status field's DF.Literal type hint on line 40, which only defines: "Not Deployed", "Ready To Deploy", "Deploying", "Deployed", "Failed", "Stopped".
This inconsistency could cause issues with form rendering or validation.
Proposed fix: Update the type hint to include the new status values
- status: DF.Literal["Not Deployed", "Ready To Deploy", "Deploying", "Deployed", "Failed", "Stopped"]
+ status: DF.Literal["Not Deployed", "Ready To Deploy", "Deploying", "Deployed", "Failed", "Stopped", "Building", "Build Failed"]🤖 Prompt for AI Agents
In `@nano_press/nano_press/doctype/frappe_site/frappe_site.py` around lines 59 -
78, The status values set by _check_custom_image_status ("Building" and "Build
Failed") are missing from the DF.Literal type for the status field; update the
status field's DF.Literal definition (the "status" field on the Frappe Site
doctype) to include "Building" and "Build Failed" alongside the existing values
so form validation and rendering match the values written by
_check_custom_image_status.
| - name: Remove frappe_docker directory to ensure clean clone | ||
| ansible.builtin.file: | ||
| path: "{{ frappe_docker_dir }}" | ||
| state: absent | ||
|
|
||
| - name: Clone frappe_docker repo (fresh) | ||
| ansible.builtin.git: | ||
| repo: "https://github.com/frappe/frappe_docker" | ||
| dest: "{{ frappe_docker_dir }}" | ||
| update: yes | ||
| force: yes | ||
| clone: yes |
There was a problem hiding this comment.
Avoid unconditional repo wipe/clone when build is skipped.
Right now the repo is deleted and re‑cloned even if image_exists and force_build is false, which adds needless network/file churn and can fail offline while no build is intended. Gate these steps on “build needed” and move the image existence check before them.
🔧 Proposed change (gate cleanup/clone on build-needed)
- - name: Remove frappe_docker directory to ensure clean clone
+ - name: Remove frappe_docker directory to ensure clean clone
ansible.builtin.file:
path: "{{ frappe_docker_dir }}"
state: absent
+ when: image_exists.stdout | length == 0 or force_build
- - name: Clone frappe_docker repo (fresh)
+ - name: Clone frappe_docker repo (fresh)
ansible.builtin.git:
repo: "https://github.com/frappe/frappe_docker"
dest: "{{ frappe_docker_dir }}"
clone: yes
accept_hostkey: yes
+ when: image_exists.stdout | length == 0 or force_build📝 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.
| - name: Remove frappe_docker directory to ensure clean clone | |
| ansible.builtin.file: | |
| path: "{{ frappe_docker_dir }}" | |
| state: absent | |
| - name: Clone frappe_docker repo (fresh) | |
| ansible.builtin.git: | |
| repo: "https://github.com/frappe/frappe_docker" | |
| dest: "{{ frappe_docker_dir }}" | |
| update: yes | |
| force: yes | |
| clone: yes | |
| - name: Remove frappe_docker directory to ensure clean clone | |
| ansible.builtin.file: | |
| path: "{{ frappe_docker_dir }}" | |
| state: absent | |
| when: image_exists.stdout | length == 0 or force_build | |
| - name: Clone frappe_docker repo (fresh) | |
| ansible.builtin.git: | |
| repo: "https://github.com/frappe/frappe_docker" | |
| dest: "{{ frappe_docker_dir }}" | |
| clone: yes | |
| when: image_exists.stdout | length == 0 or force_build |
🤖 Prompt for AI Agents
In `@nano_press/nano_press/utils/ansible/playbooks/build_custom_image.yml` around
lines 32 - 41, The tasks that unconditionally remove and re-clone the
frappe_docker repo ("Remove frappe_docker directory to ensure clean clone" and
"Clone frappe_docker repo (fresh)") should be executed only when a build is
actually required; compute a boolean like build_needed (e.g., set_fact:
build_needed: "{{ not image_exists or force_build }}") or evaluate the existing
image existence check earlier, move the image existence check task before the
cleanup/clone, and add a when: build_needed (or equivalent condition using
image_exists and force_build) to both the file removal and git clone tasks so
the repo is not wiped/cloned when image_exists is true and force_build is false.
| } catch (error) { | ||
| console.error('Deployment failed:', error); | ||
| this.deploying = false; | ||
| this.deploymentStatus = 'Deployment failed. Please try again.'; | ||
|
|
||
| if (typeof toast !== 'undefined') { | ||
| const errorMessage = error.message || 'Deployment failed. Please check your configuration.'; | ||
| toast(errorMessage, { type: 'danger' }); | ||
| } |
There was a problem hiding this comment.
Polling interval not cleared on deployment failure.
When an exception is caught in startDeployment(), the deploying flag is set to false but the polling interval from a potential earlier resumeDeployment() call (line 261) is not cleared. This could result in a zombie polling loop.
Proposed fix
} catch (error) {
console.error('Deployment failed:', error);
+ if (this.pollIntervalId) {
+ clearInterval(this.pollIntervalId);
+ this.pollIntervalId = null;
+ }
this.deploying = false;
this.deploymentStatus = 'Deployment failed. Please try again.';📝 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.
| } catch (error) { | |
| console.error('Deployment failed:', error); | |
| this.deploying = false; | |
| this.deploymentStatus = 'Deployment failed. Please try again.'; | |
| if (typeof toast !== 'undefined') { | |
| const errorMessage = error.message || 'Deployment failed. Please check your configuration.'; | |
| toast(errorMessage, { type: 'danger' }); | |
| } | |
| } catch (error) { | |
| console.error('Deployment failed:', error); | |
| if (this.pollIntervalId) { | |
| clearInterval(this.pollIntervalId); | |
| this.pollIntervalId = null; | |
| } | |
| this.deploying = false; | |
| this.deploymentStatus = 'Deployment failed. Please try again.'; | |
| const errorMessage = error.message || 'Deployment failed. Please check your configuration.'; | |
| toast(errorMessage, { type: 'danger' }); | |
| } |
🤖 Prompt for AI Agents
In `@nano_press/templates/components/core/summary.html` around lines 293 - 300, In
startDeployment's catch block you need to stop the polling started by
resumeDeployment to avoid a zombie loop: call
clearInterval(this.pollingInterval) (or clearInterval(this.deploymentInterval)
if that's the name used) and set that field to null/undefined before/when
setting this.deploying = false and this.deploymentStatus; update the catch block
in startDeployment to perform this cleanup so any existing interval is cleared
and won't continue polling after a failure.
| </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"> |
There was a problem hiding this comment.
Incomplete Tailwind gradient classes have no effect.
The classes from-gray-50 to-white require a gradient direction class like bg-gradient-to-br to work. Without it, these utility classes have no visual effect.
🎨 Suggested 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-br from-gray-50 to-white border border-gray-200 rounded-xl hover:shadow-xl transition-all duration-300 hover:-translate-y-1">Apply the same fix to lines 25 and 37.
Also applies to: 25-25, 37-37
🤖 Prompt for AI Agents
In `@nano_press/templates/components/landing/features.html` at line 13, The
Tailwind gradient color utilities on the container div with 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" are ineffective because
there's no gradient direction; update that class to include a gradient direction
such as "bg-gradient-to-br" (or another desired direction) so it becomes
"bg-gradient-to-br from-gray-50 to-white …", and apply the same change to the
other two similar divs that use "from-gray-50 to-white" (the blocks at the other
occurrences).
| <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> |
There was a problem hiding this comment.
Add rel="noopener noreferrer" to external link.
External links with target="_blank" should include rel="noopener noreferrer" to prevent potential tab-nabbing attacks where the opened page could access window.opener.
🔒 Suggested 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.
| <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 - 32, The
external anchor element that renders the "View on GitHub" button (the <a> with
href="https://github.com/BuildWithHussain/nano_press" and target="_blank")
should include rel="noopener noreferrer" to mitigate tab-napping; update that
<a> tag to add rel="noopener noreferrer" alongside the existing target
attribute.
| <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-white border-2 border-white rounded-lg hover:bg-white hover:text-gray-600 transition-all duration-200"> |
There was a problem hiding this comment.
Add rel="noopener noreferrer" to the external link.
Line 24 opens a new tab with target="_blank" but lacks rel, which exposes users to tabnabbing.
🔒 Suggested fix
- <a href="https://github.com/BuildWithHussain/nano_press"
- target="_blank"
+ <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-white border-2 border-white rounded-lg hover:bg-white hover:text-gray-600 transition-all duration-200">📝 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.
| <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-white border-2 border-white rounded-lg hover:bg-white hover:text-gray-600 transition-all duration-200"> | |
| <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-white border-2 border-white rounded-lg hover:bg-white hover:text-gray-600 transition-all duration-200"> |
🤖 Prompt for AI Agents
In `@nano_press/www/index.html` around lines 24 - 26, The external GitHub link
anchor element that uses target="_blank" (the <a> with
href="https://github.com/BuildWithHussain/nano_press") is missing the rel
attribute; update that anchor to include rel="noopener noreferrer" to prevent
tabnabbing and ensure the new tab cannot access the opener window.
| def get_frappe_versions(): | ||
| return frappe.db.get_all( | ||
| "App Version", | ||
| fields=["version", "scrubbed_version"], | ||
| order_by="creation asc", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's verify the file exists and examine the current code
find . -type f -name "self_host.py" | head -5Repository: BuildWithHussain/nano_press
Length of output: 198
🏁 Script executed:
# Search for the get_frappe_versions function
rg "def get_frappe_versions" -A 10Repository: BuildWithHussain/nano_press
Length of output: 381
🏁 Script executed:
# Let's also check for documentation or comments about frappe.db.get_all vs frappe.get_list
rg "frappe\.(db\.)?get_(all|list)" --type py | head -20Repository: BuildWithHussain/nano_press
Length of output: 452
🌐 Web query:
Frappe framework frappe.db.get_all vs frappe.get_list permission checks
💡 Result:
Short answer: frappe.db.get_list (aka frappe.get_list) enforces Frappe permission rules for the current session user; frappe.db.get_all (aka frappe.get_all) bypasses document/doctype permission checks and returns all matching rows. Use get_list when you want permission filtering; use get_all only when you intentionally need to ignore permissions (or when running as an admin/system process). [1][2][3]
Notes / quick pointers:
- Documentation: get_list applies user permissions; get_all fetches all records without applying permissions. [2][3]
- Newer APIs (frappe.qb.get_query) default to ignoring permissions (ignore_permissions=True); set ignore_permissions=False to get permission-enforced results. [4]
- Many docs and community guidance recommend preferring get_list (or explicit permission-aware queries) to avoid exposing unauthorized records. [1][5]
Sources:
- Frappe Database API: get_all / get_list behavior. [1][2]
- frappe.qb.get_query permissions (ignore_permissions). [4]
- Frappe community / security guidance recommending get_list over get_all. [5]
🌐 Web query:
Frappe db.get_all bypasses permission checks security issue
💡 Result:
Short answer
Yes — using frappe.db.get_all (and some whitelisted endpoints) can bypass built‑in permission checks if used improperly; Frappe’s docs and advisories explicitly warn that certain DB helpers and whitelisted methods do not enforce user permissions and have led to real vulnerabilities. Follow safe alternatives and patches. [1][2][3][4]
What’s happening (brief)
- Some low‑level helpers (e.g., frappe.db.get_all / frappe.get_doc / certain whitelisted API methods) do not apply user permission checks by default, so calling them from code that accepts untrusted input or from client‑exposed whitelisted methods can expose or modify data a user shouldn’t see. Frappe guidance is to prefer permission‑aware APIs and validate server side. [1][2]
- There are real CVEs/patches where endpoints allowed file access or SQL injection that led to permission bypasses — upgrade to patched releases. [3][4]
Mitigation (actionable, short)
- Don’t call get_all/get_doc from client‑exposed whitelisted methods without server‑side permission checks. Use frappe.get_list / frappe.get_value or server methods that enforce permissions. [1]
- If you must bypass permissions for a trusted server action, do so only in server code and with explicit checks (frappe.only_for, doc.check_permission("read"), validate doctype/name lists). [1]
- Remove ignore_permissions/ignore_user_permissions arguments from client‑controllable input; never let the frontend decide to ignore permissions. [1][2]
- Upgrade Frappe to versions that contain relevant patches (see Frappe security bulletin and CVE advisories) and monitor vendor advisories for fixes. [3][4]
- Audit and restrict whitelisted methods (/api/method/*): validate all inputs, avoid dynamic SQL, and add explicit permission checks. [1][4]
References
- Frappe code security guidelines (notes on get_all/get_list and permission best practices). [1]
- Frappe community discussion: client use of get_all / ignore permissions is insecure. [2]
- Frappe security bulletin (published advisories and patched issues). [3]
- CVE entry describing file‑permission bypass and patched versions. [4]
Use permission-aware query for App Version.
frappe.db.get_all bypasses permission checks and will expose App Version data to all users regardless of read permissions. Use frappe.get_list instead to enforce user permissions.
Suggested fix
def get_frappe_versions():
- return frappe.db.get_all(
+ return frappe.get_list(
"App Version",
fields=["version", "scrubbed_version"],
order_by="creation asc",
)📝 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.
| def get_frappe_versions(): | |
| return frappe.db.get_all( | |
| "App Version", | |
| fields=["version", "scrubbed_version"], | |
| order_by="creation asc", | |
| def get_frappe_versions(): | |
| return frappe.get_list( | |
| "App Version", | |
| fields=["version", "scrubbed_version"], | |
| order_by="creation asc", | |
| ) |
🤖 Prompt for AI Agents
In `@nano_press/www/self_host.py` around lines 32 - 36, The get_frappe_versions
function uses frappe.db.get_all which bypasses permissions; change it to use
frappe.get_list so reads respect user permissions: replace the call to
frappe.db.get_all("App Version", fields=[...], order_by="creation asc") with
frappe.get_list("App Version", fields=[...], order_by="creation asc") and ensure
you do not pass ignore_permissions=True so permission checks remain enforced
(keep the function name get_frappe_versions and the same fields/order_by
parameters).
apps.html: - Add x-cloak to custom app dialog to prevent flash on page load - Fixes FOUC (Flash of Unstyled Content) where dialog appeared briefly before Alpine.js initialized self_host.py: - Replace frappe.db.get_list() with frappe.get_all() in get_user_servers() and get_apps() - Replace frappe.db.get_all() with frappe.get_all() in get_frappe_versions() - All three functions now consistently use frappe.get_all() following Frappe ORM best practices
Summary
Major refactoring of the self-hosting installer UI components to eliminate legacy dead code, remove defensive coding patterns, and improve overall code maintainability. This PR cleans up ~300 lines of unnecessary code across 5 files without changing any functionality.
Changes by File
1. server.html (commit 204d376)
x-datacomponent and merge copy functionality into mainserverSetup()validateIP()methodcopiedpropertytypeof toast !== 'undefined'checks (toast always available)serverSetup()as methods:createServerDoc(),pingServer(),verifyAndCreateServer()2. apps.html (commit 3b774f3)
typeof toast !== 'undefined'checks (4 instances)3. domain.html (commit 9ad1abc)
typeof toast !== 'undefined'checkprevStep()method (use inherited from parent)verify_domain()global function intodomainSetup()asverifyDomain()methodinit()function andx-initdirective from HTML4. summary.html + self-host.html (commit a581c3a)
summary.html:
typeof toast !== 'undefined'checks (19 instances!)buildingImageproperty and assignmentsprevStep()method (use inherited from parent)self-host.html:
frappeInstaller()parent componentshowAdvanced,generatedSSHKey,showCustomAppDialog,customAppForm,domain,domainVerified,domainStatusMessage,isLoading,deploying,deployComplete,deployStep,deploymentStatus,copiedcopyToClipboard(),toggleApp(),getSelectedAppsNames(),startDeployment(),visitSite()stepNames, store getters,nextStep(),prevStep()Why These Changes Matter
Before this refactoring:
typeof toast !== 'undefined'checks littered throughout despite toast always being availableAfter this refactoring:
Testing
All functionality remains unchanged:
Impact
Summary by CodeRabbit
New Features
Bug Fixes
Style
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.