Compare commits

...
10 Commits
21 changed files with 993 additions and 113 deletions

No files matched your search

+2 -2
View File
@@ -37,7 +37,7 @@ RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64")
" - busybox" \
" - tzdata" \
" - docker-cli" \
" - docker-compose=5.5.0-r2" \
" - docker-compose=5.5.0-r5" \
" - docker-cli-buildx" \
" - sqlite" \
" - postgresql-client" \
@@ -99,7 +99,7 @@ RUN cp -r node_modules/better-sqlite3/build /tmp/better-sqlite3-build \
&& rm -rf node_modules/@types /tmp/better-sqlite3-build
# Build Go collector
FROM --platform=$BUILDPLATFORM golang:1.25.13 AS go-builder
FROM --platform=$BUILDPLATFORM golang:1.26.8 AS go-builder
ARG TARGETARCH
WORKDIR /app
COPY collector/ ./collector/
+1 -1
View File
@@ -46,7 +46,7 @@ RUN cp -r node_modules/better-sqlite3/build /tmp/better-sqlite3-build \
# -----------------------------------------------------------------------------
# Stage 2: Go Collector Builder
# -----------------------------------------------------------------------------
FROM golang:1.25.13 AS go-builder
FROM golang:1.26.8 AS go-builder
WORKDIR /app
COPY collector/ ./collector/
RUN cd collector && CGO_ENABLED=0 go build -o /app/bin/collection-worker .
+2 -2
View File
@@ -23,7 +23,7 @@
# -----------------------------------------------------------------------------
# Alpine edge has docker-cli for riscv64 but NOT docker-compose.
# Build it from source since Go has excellent riscv64 cross-compilation.
FROM --platform=$BUILDPLATFORM golang:1.25.9-alpine AS compose-builder
FROM --platform=$BUILDPLATFORM golang:1.26.8-alpine AS compose-builder
ARG TARGETARCH
RUN apk add --no-cache git
@@ -72,7 +72,7 @@ RUN MAKEFLAGS="-j$(nproc)" npm ci --omit=dev --ignore-scripts \
# -----------------------------------------------------------------------------
# Stage 4: Go Collector Builder (cross-compile from build platform)
# -----------------------------------------------------------------------------
FROM --platform=$BUILDPLATFORM golang:1.25.13 AS go-builder
FROM --platform=$BUILDPLATFORM golang:1.26.8 AS go-builder
ARG TARGETARCH
WORKDIR /app
COPY collector/ ./collector/
+245
View File
@@ -0,0 +1,245 @@
# Dockhand License
Copyright (c) 2025-2026 Finsys Jaroslaw Krochmalski
Last updated: 14 September 2026
## How to read this document
The document has two parts:
**Part A — Business Source License 1.1.** The license under which Dockhand is published. It applies to everyone who obtains a copy of Dockhand, whether from GitHub, from a container registry or bundled in the application. The text of the Business Source License itself is reproduced unchanged; Licensor's choices are in the "Parameters" section. Within Part A, "this License" means Part A only.
**Part B — Dockhand Commercial License Terms.** These apply only if you purchase a Commercial License (currently the SMB or Enterprise tier). They add rights and obligations on top of Part A and never take away rights you have under Part A.
## Summary
- Free for everyone, no license key, any number of users or environments: development, testing, staging, CI/CD, evaluation, demos, training — including inside companies.
- Free in production: individuals for personal use (home labs, hobby projects); non-profit and charitable organizations; schools, universities and non-commercial research institutions.
- Paid (Commercial License, Part B): production use by or for any business, public body or other organization not listed above.
- Never permitted without a separate written agreement: offering Dockhand itself, or a fork of it, as a hosted, managed or SaaS service to third parties.
- Each released version becomes Apache 2.0 four years after its release.
If this summary and Parts A or B differ, Parts A and B govern.
## General terms (these apply to everyone)
The following apply to every user of the Licensed Work, whether you use it for free under Part A or under a Commercial License under Part B.
**Data and privacy.** The Licensed Work stores all data locally on your infrastructure and contains no telemetry or analytics. It does not contact Licensor's servers; any License Key is validated locally. Licensor does not collect, access or process data stored in your installation. You are solely responsible for the security, backup and protection of that data. Licensor processes only the personal data needed to handle Orders, License Keys and support requests, as described in the Privacy Policy at https://dockhand.pro.
**Ownership.** The Licensed Work is and remains the property of Licensor. All rights not expressly granted under Part A or Part B are reserved.
**Trademarks.** "Dockhand", "Finsys" and the associated logos are trademarks of Licensor. Nothing in this document grants any trademark rights beyond those expressly required by Part A.
**Feedback.** Licensor may use any suggestions, ideas, enhancement requests or other feedback you provide regarding the Licensed Work, without obligation to you.
**Limitation of liability.** To the maximum extent permitted by applicable law, Licensor is not liable for any indirect, incidental, special, consequential or punitive damages, or for loss of profits, revenue, data, goodwill or use, business interruption, security breaches, data corruption, container management errors, or third-party actions, arising out of or in connection with the Licensed Work or your use of or inability to use it, under any theory of liability, even if advised of the possibility of such damages. Nothing here excludes or limits liability that cannot be excluded or limited under applicable law, including liability for damage caused intentionally. This applies to every user; a Commercial Licensee's liability terms are in B.12.
---
# Part A — Business Source License 1.1
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
"Business Source License" is a trademark of MariaDB Corporation Ab.
### Parameters
**Licensor:** Finsys Jaroslaw Krochmalski
**Licensed Work:** Dockhand, meaning all software published by Licensor in the repository https://github.com/Finsys/dockhand (including the Dockhand server and its agent and updater components) and the binaries and container images Licensor builds from that repository. The Licensed Work is (c) 2025-2026 Finsys Jaroslaw Krochmalski.
**Additional Use Grant:**
You may make production use of the Licensed Work if, and only for as long as, at least one of the following applies to you:
(a) Personal use. You are an individual using the Licensed Work for personal, non-commercial purposes (for example a home lab or a hobby project), and not for the benefit of any business or other organization.
(b) Non-profit use. You are a non-profit or charitable organization, recognized as such under the law of your jurisdiction, using the Licensed Work for your own non-profit activities.
(c) Educational and research use. You are a school, college, university or non-commercial research institution using the Licensed Work for teaching, learning or non-commercial research.
(d) Commercial License. You hold a valid Commercial License granted by Licensor under Part B of the Dockhand License or under another written agreement with Licensor, and your production use is within the scope and term of that Commercial License.
(e) Cure period. Your production use is not covered by (a) to (d) and Licensor has notified you of this in writing: you may continue that production use for thirty (30) days after the notice, solely in order to obtain a Commercial License or to stop the production use. If you do either within that period, Licensor will treat the earlier production use as cured.
Any other production use — including production use by or on behalf of a company, sole trader or self-employed individual, public body or any other organization — requires a Commercial License from Licensor.
Use of the Licensed Work by a contractor or service provider acting solely on behalf of a person or organization covered by (a) to (d), and only for that person's or organization's own infrastructure, is treated as use by that person or organization.
"Production use" means using the Licensed Work to manage, monitor or operate containers, images, stacks, volumes, networks or Docker environments that serve your customers or end users, that run your live business or organizational systems, or on whose availability you otherwise rely for your day-to-day operations. Using the Licensed Work for development, testing, staging, CI/CD, evaluation, demonstration, training or learning purposes is not production use, regardless of who you are and regardless of the number of users, installations or environments involved, and is permitted under this License without a Commercial License.
Nothing in this Additional Use Grant permits you to offer the Licensed Work, or a derivative work of the Licensed Work, to third parties as a hosted service, managed service or software-as-a-service offering whose primary value to users is Docker container management functionality substantially similar to the Licensed Work. Such an offering requires a separate written agreement with Licensor.
**Eligibility, enforcement and License Keys.** The rights in the Additional Use Grant depend on your eligibility, and Enterprise Features are gated by a License Key. Accordingly:
(i) You must not misrepresent your eligibility for any of the categories in the Additional Use Grant (for example, claiming personal, non-profit or educational use that does not genuinely apply to you).
(ii) You must not share, publish or distribute a License Key, nor circumvent or attempt to circumvent any license-enforcement mechanism (for example, to enable Enterprise Features without a valid Commercial License).
(iii) Licensor may request reasonable verification of your eligibility for personal, non-profit or educational use, and you will provide reasonable documentation on request. This does not permit Licensor to access data stored in your installation (see the General terms above).
**Change Date:** Four years from the date on which a given version of the Licensed Work is first publicly distributed by Licensor.
**Change License:** Apache License, Version 2.0
### Terms
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
MariaDB hereby grants you permission to use this License's text to license your works, and to refer to it using the trademark "Business Source License", as long as you comply with the Covenants of Licensor below.
### Covenants of Licensor
In consideration of the right to use this License's text and the "Business Source License" name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where "compatible" means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation.
2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text "None".
3. To specify a Change Date.
4. Not to modify this License in any other way.
### Notice
The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work will eventually be made available under an Open Source License, as stated in this License.
For licensing inquiries, commercial licensing or enterprise features, email enterprise@dockhand.pro
---
# Part B — Dockhand Commercial License Terms
## B.1 Scope and relationship to Part A
1. Part B applies between Finsys Jaroslaw Krochmalski ("Licensor", "Finsys") and the person or entity named in an Order ("Licensee", "you") from the earlier of: (a) Licensee placing an Order, or (b) Licensee activating a License Key.
2. Part B supplements Part A. Part A continues to apply to the Licensed Work. Nothing in Part B limits, conditions or withdraws any right granted to you under Part A.
3. The Order, Part B and Part A together form the entire agreement regarding the Commercial License and supersede any prior or contemporaneous agreement on that subject. In case of conflict, the Order prevails over Part B as to tier, Scope, fees and License Term; Part B prevails over the Order in all other respects; and Part A always prevails as to the rights it grants.
4. The Commercial License is intended for businesses, public bodies and other organizations. Licensor does not offer it to consumers.
## B.2 Definitions
"Commercial License" means the license granted under B.3, in the tier stated in the Order. Licensor currently offers the "SMB" and "Enterprise" tiers. Tiers, prices, included features and Scope limits are described at https://dockhand.pro and in the Order.
"Order" means the order confirmation, invoice, quote accepted by Licensee, purchase order accepted by Licensor, or other document recording Licensee's purchase, tier, Scope and License Term.
"License Key" means the cryptographic key issued by Licensor that enables Enterprise Features and identifies a Commercial License.
"Enterprise Features" means the features of the Licensed Work that are enabled with a License Key. At the date of these terms they include LDAP/Active Directory integration, role-based access control with environment scoping, and audit logging. The current list is maintained at https://dockhand.pro. The source code of the Enterprise Features is published in the Dockhand source repository and is part of the Licensed Work under Part A.
"License Term" means the subscription period stated in the Order or, if none is stated, twelve (12) months from activation of the License Key.
"Scope" means the limits stated in the Order and in the tier description, such as the number of installations, environments or users.
"Licensed Work" and "production use" have the meanings given in Part A.
## B.3 Grant
1. For the License Term, subject to payment of the fees in the Order and to Part B, Licensor grants Licensee a non-exclusive, non-transferable (except under B.7) license to:
(a) make production use of the Licensed Work, within the Scope;
(b) receive a License Key and use the Enterprise Features included in Licensee's tier, within the Scope; and
(c) receive the support included in Licensee's tier (B.8).
2. Licensee's employees and contractors, and any affiliates named in the Order, may exercise these rights on Licensee's behalf and for Licensee's own infrastructure. Licensee is responsible for their compliance with Part A and Part B.
## B.4 What the Commercial License does not include
1. The Commercial License does not permit Licensee to:
(a) offer the Licensed Work or the Enterprise Features to third parties as a hosted service, managed service or software-as-a-service offering (see the Additional Use Grant in Part A); this requires a separate written agreement with Licensor;
(b) share, publish, sublicense, rent, resell or otherwise make a License Key available to anyone outside the Scope;
(c) make production use of the Licensed Work, or use Enterprise Features, outside the Scope or after the end of the License Term.
2. Licensee's rights in the Licensed Work under Part A — including the rights to copy, modify, create derivative works and redistribute it — are unaffected by Part B.
## B.5 Term, renewal and expiry
1. The License Term starts on the date stated in the Order or, if none is stated, on activation of the License Key.
2. Renewal is not automatic. To keep the Commercial License, Licensee must purchase a renewal before the License Term ends.
3. When the License Term ends, or the Commercial License is terminated:
(a) Enterprise Features are disabled automatically;
(b) the Licensed Work continues to function with its remaining features, and all data, configuration and settings are preserved;
(c) Part A alone governs any further use. A Licensee that is not covered by clauses (a) to (c) of the Additional Use Grant must therefore renew or stop production use. The thirty (30) day cure period in the Additional Use Grant applies after Licensor's written notice.
## B.6 Fees and taxes
Fees are as stated in the Order, payable as set out in the Order or invoice, and exclusive of VAT and other applicable taxes, which Licensee pays in addition. Fees are non-refundable except where required by applicable law or agreed by Licensor in writing.
## B.7 Transfer
1. Reassignment. Licensee may move a Commercial License to another installation within its organization (for example on hardware replacement or migration), provided the License Key is removed from the previous installation and the number of active installations stays within the Scope.
2. Business transfers. Licensee may transfer the Commercial License to its legal successor in a merger, acquisition, reorganization or sale of all or substantially all of its assets, by written notice to enterprise@dockhand.pro within thirty (30) days of the transfer, stating the successor's legal name and contact details and confirming that the successor accepts Part A and Part B.
3. No other transfer, resale or sublicensing of a Commercial License or License Key is permitted without Licensor's prior written consent.
## B.8 Support
1. Licensor provides the support level included in Licensee's tier on a commercially reasonable, best-effort basis, through enterprise@dockhand.pro.
2. Unless separately agreed in writing, Licensor does not guarantee response or resolution times, the availability of updates, patches or new versions, or compatibility with future operating systems, Docker versions or other software, and no service level agreement applies.
## B.9 Data and privacy
The Licensed Work stores all data locally on Licensee's infrastructure and contains no telemetry or analytics. License Keys are validated locally; the Licensed Work does not contact Licensor's servers. Licensor does not collect, access or process data stored in Licensee's installation. Licensee is solely responsible for the security, backup and protection of that data. Licensor processes the personal data needed to handle Orders, License Keys and support requests as described in the Privacy Policy at https://dockhand.pro.
## B.10 Ownership, trademarks and feedback
1. The Licensed Work, including the Enterprise Features, is and remains the property of Licensor. All rights not expressly granted in Part A or Part B are reserved.
2. "Dockhand", "Finsys" and the associated logos are trademarks of Licensor. Neither Part A nor Part B grants any trademark rights beyond those expressly required by Part A.
3. Licensor may use any feedback, suggestions or enhancement requests Licensee provides, without obligation to Licensee.
## B.11 Warranty disclaimer
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK, INCLUDING THE ENTERPRISE FEATURES, IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. LICENSOR DOES NOT WARRANT THAT THE LICENSED WORK WILL MEET LICENSEE'S REQUIREMENTS, BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE, THAT ERRORS WILL BE CORRECTED, OR THAT THE LICENSED WORK WILL BE COMPATIBLE WITH LICENSEE'S HARDWARE, SOFTWARE OR NETWORK ENVIRONMENT. THE STATUTORY WARRANTY FOR DEFECTS (RĘKOJMIA) IS EXCLUDED TO THE EXTENT PERMITTED BY LAW.
LICENSEE ACKNOWLEDGES THAT THE LICENSED WORK IS USED TO MANAGE DOCKER CONTAINERS AND RELATED INFRASTRUCTURE, AND THAT DEFECTS OR IMPROPER USE MAY RESULT IN DATA LOSS, DOWNTIME, SYSTEM DAMAGE OR SECURITY INCIDENTS. LICENSEE ASSUMES THAT RISK.
## B.12 Limitation of liability
1. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR IS NOT LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR LOSS OF PROFITS, REVENUE, DATA, GOODWILL OR USE, BUSINESS INTERRUPTION, SECURITY BREACHES, DATA CORRUPTION, CONTAINER MANAGEMENT ERRORS, THIRD-PARTY ACTIONS OR INTEGRATIONS, OR THE COST OF SUBSTITUTE GOODS OR SERVICES, ARISING OUT OF OR IN CONNECTION WITH PART A, PART B OR THE USE OF OR INABILITY TO USE THE LICENSED WORK, UNDER ANY THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
2. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR'S TOTAL AGGREGATE LIABILITY ARISING OUT OF OR IN CONNECTION WITH PART A AND PART B IS LIMITED TO THE FEES PAID BY LICENSEE UNDER THE ORDER IN THE TWELVE (12) MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM.
3. Nothing in Part B excludes or limits liability that cannot be excluded or limited under applicable law, including liability for damage caused intentionally.
4. These limitations are an essential basis of the bargain between Licensee and Licensor; Licensor would not offer the Commercial License without them.
## B.13 Termination
1. Licensor may terminate the Commercial License by written notice if Licensee materially breaches Part A or Part B and, where the breach can be cured, fails to cure it within thirty (30) days of written notice.
2. Licensee may stop using the Commercial License at any time. Fees already paid are not refunded.
3. On termination, B.5(3) applies. B.6 and B.10 to B.16 survive termination or expiry.
## B.14 Governing law and disputes
Part B, and any dispute arising out of or in connection with it, is governed by the laws of Poland, excluding its conflict-of-law rules and the UN Convention on Contracts for the International Sale of Goods. The courts of Warsaw, Poland have exclusive jurisdiction. Part B is drafted in English; in case of discrepancy with any translation, the English text prevails.
## B.15 General
1. Changes. Licensor may publish updated Commercial License Terms. Updated terms apply to Orders placed and renewals purchased after publication. The terms in force when an Order is placed govern that License Term.
2. Assignment. Licensee may assign Part B only as permitted in B.7. Licensor may assign Part B to a successor of the Dockhand business on written notice.
3. Severability. If any provision of Part B is unenforceable, it is reformed only to the extent necessary to make it enforceable, and the remaining provisions remain in force.
4. Waiver. Licensor's failure to enforce any provision is not a waiver of it.
5. Independent contractors. Nothing in Part B creates a partnership, joint venture, agency or employment relationship.
6. Export. Licensee will comply with applicable export and import laws in connection with its use of the Licensed Work.
7. Notices. Notices to Licensor go to enterprise@dockhand.pro; notices to Licensee go to the contact details in the Order.
## B.16 Licensor
Finsys Jaroslaw Krochmalski
ul. Borki 6, 05-119 Jozefow, Poland
VAT ID: PL7121835977 · REGON: 061576391
enterprise@dockhand.pro · https://dockhand.pro
---
# Third-party components
The Licensed Work incorporates open source components under their own licenses, including SvelteKit (MIT), Svelte (MIT), Dockerode (Apache 2.0), xterm.js (MIT), Drizzle ORM (Apache 2.0), Tailwind CSS (MIT), shadcn-svelte (MIT) and Lucide Icons (ISC). The full list is in the package manifest files. Your use of those components is governed by their respective licenses; nothing in Part A or Part B limits your rights under them.
+451 -79
View File
@@ -1,128 +1,500 @@
Business Source License 1.1
DOCKHAND LICENSE
Copyright (c) 2025-2026 Finsys Jaroslaw Krochmalski
Last updated: 14 September 2026
HOW TO READ THIS DOCUMENT
The document has two parts:
Part A - Business Source License 1.1. The license under which Dockhand is
published. It applies to everyone who obtains a copy of Dockhand, whether from
GitHub, from a container registry or bundled in the application. The text of the
Business Source License itself is reproduced unchanged; Licensor's choices are
in the "Parameters" section. Within Part A, "this License" means Part A only.
Part B - Dockhand Commercial License Terms. These apply only if you purchase a
Commercial License (currently the SMB or Enterprise tier). They add rights and
obligations on top of Part A and never take away rights you have under Part A.
SUMMARY
- Free for everyone, no license key, any number of users or environments:
development, testing, staging, CI/CD, evaluation, demos, training - including
inside companies.
- Free in production: individuals for personal use (home labs, hobby projects);
non-profit and charitable organizations; schools, universities and
non-commercial research institutions.
- Paid (Commercial License, Part B): production use by or for any business,
public body or other organization not listed above.
- Never permitted without a separate written agreement: offering Dockhand
itself, or a fork of it, as a hosted, managed or SaaS service to third
parties.
- Each released version becomes Apache 2.0 four years after its release.
If this summary and Parts A or B differ, Parts A and B govern.
GENERAL TERMS (THESE APPLY TO EVERYONE)
The following apply to every user of the Licensed Work, whether you use it for
free under Part A or under a Commercial License under Part B.
Data and privacy. The Licensed Work stores all data locally on your
infrastructure and contains no telemetry or analytics. It does not contact
Licensor's servers; any License Key is validated locally. Licensor does not
collect, access or process data stored in your installation. You are solely
responsible for the security, backup and protection of that data. Licensor
processes only the personal data needed to handle Orders, License Keys and
support requests, as described in the Privacy Policy at https://dockhand.pro.
Ownership. The Licensed Work is and remains the property of Licensor. All rights
not expressly granted under Part A or Part B are reserved.
Trademarks. "Dockhand", "Finsys" and the associated logos are trademarks of
Licensor. Nothing in this document grants any trademark rights beyond those
expressly required by Part A.
Feedback. Licensor may use any suggestions, ideas, enhancement requests or other
feedback you provide regarding the Licensed Work, without obligation to you.
Limitation of liability. To the maximum extent permitted by applicable law,
Licensor is not liable for any indirect, incidental, special, consequential or
punitive damages, or for loss of profits, revenue, data, goodwill or use,
business interruption, security breaches, data corruption, container management
errors, or third-party actions, arising out of or in connection with the
Licensed Work or your use of or inability to use it, under any theory of
liability, even if advised of the possibility of such damages. Nothing here
excludes or limits liability that cannot be excluded or limited under applicable
law, including liability for damage caused intentionally. This applies to every
user; a Commercial Licensee's liability terms are in B.12.
===============================================================================
PART A - BUSINESS SOURCE LICENSE 1.1
===============================================================================
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
"Business Source License" is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
PARAMETERS
Parameters
Licensor: Finsys Jaroslaw Krochmalski
Licensor: Finsys / Jarek Krochmalski
Licensed Work: Dockhand, meaning all software published by Licensor in the
repository https://github.com/Finsys/dockhand (including the Dockhand server and
its agent and updater components) and the binaries and container images Licensor
builds from that repository. The Licensed Work is (c) 2025-2026 Finsys Jaroslaw
Krochmalski.
Licensed Work: Dockhand
The Licensed Work is (c) 2025-2026 Finsys / Jarek Krochmalski.
Additional Use Grant:
Additional Use Grant: You may use the Licensed Work for any purpose, including
production use, provided that you do not offer the Licensed
Work, or any derivative work of the Licensed Work, to third
parties as a commercial hosted service, managed service, or
software-as-a-service (SaaS) offering where the primary value
proposition to users is Docker container management
functionality substantially similar to the Licensed Work.
You may make production use of the Licensed Work if, and only for as long as, at
least one of the following applies to you:
For clarity, the following uses are explicitly permitted
without any restriction:
(a) Personal use. You are an individual using the Licensed Work for personal,
non-commercial purposes (for example a home lab or a hobby project), and not for
the benefit of any business or other organization.
(a) Personal use, including home labs and hobby projects
(b) Internal business use within your organization, regardless
of the number of Docker environments managed
(c) Use by non-profit organizations and charitable entities
(d) Educational, academic, and research purposes
(e) Evaluation, testing, development, and demonstration purposes
(f) Embedding or integrating the Licensed Work into internal
tools or platforms that are not offered commercially to
third parties
(g) Use by managed service providers (MSPs) to manage Docker
infrastructure on behalf of their clients, provided the
MSP does not offer Dockhand itself as the service
(b) Non-profit use. You are a non-profit or charitable organization, recognized
as such under the law of your jurisdiction, using the Licensed Work for your own
non-profit activities.
Change Date: January 1, 2029
(c) Educational and research use. You are a school, college, university or
non-commercial research institution using the Licensed Work for teaching,
learning or non-commercial research.
Change License: Apache License, Version 2.0
(d) Commercial License. You hold a valid Commercial License granted by Licensor
under Part B of the Dockhand License or under another written agreement with
Licensor, and your production use is within the scope and term of that
Commercial License.
-----------------------------------------------------------------------------
(e) Cure period. Your production use is not covered by (a) to (d) and Licensor
has notified you of this in writing: you may continue that production use for
thirty (30) days after the notice, solely in order to obtain a Commercial
License or to stop the production use. If you do either within that period,
Licensor will treat the earlier production use as cured.
Terms
Any other production use - including production use by or on behalf of a
company, sole trader or self-employed individual, public body or any other
organization - requires a Commercial License from Licensor.
Use of the Licensed Work by a contractor or service provider acting solely on
behalf of a person or organization covered by (a) to (d), and only for that
person's or organization's own infrastructure, is treated as use by that person
or organization.
"Production use" means using the Licensed Work to manage, monitor or operate
containers, images, stacks, volumes, networks or Docker environments that serve
your customers or end users, that run your live business or organizational
systems, or on whose availability you otherwise rely for your day-to-day
operations. Using the Licensed Work for development, testing, staging, CI/CD,
evaluation, demonstration, training or learning purposes is not production use,
regardless of who you are and regardless of the number of users, installations
or environments involved, and is permitted under this License without a
Commercial License.
Nothing in this Additional Use Grant permits you to offer the Licensed Work, or
a derivative work of the Licensed Work, to third parties as a hosted service,
managed service or software-as-a-service offering whose primary value to users
is Docker container management functionality substantially similar to the
Licensed Work. Such an offering requires a separate written agreement with
Licensor.
Eligibility, enforcement and License Keys. The rights in the Additional Use
Grant depend on your eligibility, and Enterprise Features are gated by a License
Key. Accordingly:
(i) You must not misrepresent your eligibility for any of the categories in the
Additional Use Grant (for example, claiming personal, non-profit or educational
use that does not genuinely apply to you).
(ii) You must not share, publish or distribute a License Key, nor circumvent or
attempt to circumvent any license-enforcement mechanism (for example, to enable
Enterprise Features without a valid Commercial License).
(iii) Licensor may request reasonable verification of your eligibility for
personal, non-profit or educational use, and you will provide reasonable
documentation on request. This does not permit Licensor to access data stored in
your installation (see the General terms above).
Change Date: Four years from the date on which a given version of the Licensed
Work is first publicly distributed by Licensor.
Change License: Apache License, Version 2.0
TERMS
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Licensor may make an Additional Use Grant, above, permitting limited production
use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
License, whichever comes first, the Licensor hereby grants you rights under the
terms of the Change License, and the rights granted in the paragraph above
terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
If your use of the Licensed Work does not comply with the requirements currently
in effect as described in this License, you must purchase a commercial license
from the Licensor, its affiliated entities, or authorized resellers, or you must
refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
All copies of the original and modified Licensed Work, and derivative works of
the Licensed Work, are subject to this License. This License applies separately
for each version of the Licensed Work and the Change Date may vary for each
version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
You must conspicuously display this License on each original or modified copy of
the Licensed Work. If you receive the Licensed Work in original or modified form
from a third party, the terms and conditions set forth in this License apply to
your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
terminate your rights under this License for the current and all other versions
of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
This License does not grant you any right in any trademark or logo of Licensor
or its affiliates (provided that you may use a trademark or logo of Licensor as
expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
"AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
MariaDB hereby grants you permission to use this License's text to license
your works, and to refer to it using the trademark "Business Source License",
as long as you comply with the Covenants of Licensor below.
MariaDB hereby grants you permission to use this License's text to license your
works, and to refer to it using the trademark "Business Source License", as long
as you comply with the Covenants of Licensor below.
-----------------------------------------------------------------------------
Covenants of Licensor
COVENANTS OF LICENSOR
In consideration of the right to use this License's text and the "Business
Source License" name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where "compatible" means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
1. To specify as the Change License the GPL Version 2.0 or any later version, or
a license that is compatible with GPL Version 2.0 or a later version, where
"compatible" means that software provided under the Change License can be
included in a program with software provided under GPL Version 2.0 or a later
version. Licensor may specify additional Change Licenses without limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text "None".
impose any additional restriction on the right granted in this License, as the
Additional Use Grant; or (b) insert the text "None".
3. To specify a Change Date.
4. Not to modify this License in any other way.
-----------------------------------------------------------------------------
Notice
NOTICE
The Business Source License (this document, or the "License") is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
-----------------------------------------------------------------------------
For licensing inquiries, commercial licensing or enterprise features, email
[email protected]
For licensing inquiries, commercial licensing, or enterprise features:
Website: https://dockhand.pro
===============================================================================
PART B - DOCKHAND COMMERCIAL LICENSE TERMS
===============================================================================
-----------------------------------------------------------------------------
B.1 SCOPE AND RELATIONSHIP TO PART A
1. Part B applies between Finsys Jaroslaw Krochmalski ("Licensor", "Finsys") and
the person or entity named in an Order ("Licensee", "you") from the earlier of:
(a) Licensee placing an Order, or (b) Licensee activating a License Key.
2. Part B supplements Part A. Part A continues to apply to the Licensed Work.
Nothing in Part B limits, conditions or withdraws any right granted to you under
Part A.
3. The Order, Part B and Part A together form the entire agreement regarding the
Commercial License and supersede any prior or contemporaneous agreement on that
subject. In case of conflict, the Order prevails over Part B as to tier, Scope,
fees and License Term; Part B prevails over the Order in all other respects; and
Part A always prevails as to the rights it grants.
4. The Commercial License is intended for businesses, public bodies and other
organizations. Licensor does not offer it to consumers.
B.2 DEFINITIONS
"Commercial License" means the license granted under B.3, in the tier stated in
the Order. Licensor currently offers the "SMB" and "Enterprise" tiers. Tiers,
prices, included features and Scope limits are described at https://dockhand.pro
and in the Order.
"Order" means the order confirmation, invoice, quote accepted by Licensee,
purchase order accepted by Licensor, or other document recording Licensee's
purchase, tier, Scope and License Term.
"License Key" means the cryptographic key issued by Licensor that enables
Enterprise Features and identifies a Commercial License.
"Enterprise Features" means the features of the Licensed Work that are enabled
with a License Key. At the date of these terms they include LDAP/Active
Directory integration, role-based access control with environment scoping, and
audit logging. The current list is maintained at https://dockhand.pro. The
source code of the Enterprise Features is published in the Dockhand source
repository and is part of the Licensed Work under Part A.
"License Term" means the subscription period stated in the Order or, if none is
stated, twelve (12) months from activation of the License Key.
"Scope" means the limits stated in the Order and in the tier description, such
as the number of installations, environments or users.
"Licensed Work" and "production use" have the meanings given in Part A.
B.3 GRANT
1. For the License Term, subject to payment of the fees in the Order and to Part
B, Licensor grants Licensee a non-exclusive, non-transferable (except under B.7)
license to:
(a) make production use of the Licensed Work, within the Scope;
(b) receive a License Key and use the Enterprise Features included in
Licensee's tier, within the Scope; and
(c) receive the support included in Licensee's tier (B.8).
2. Licensee's employees and contractors, and any affiliates named in the Order,
may exercise these rights on Licensee's behalf and for Licensee's own
infrastructure. Licensee is responsible for their compliance with Part A and
Part B.
B.4 WHAT THE COMMERCIAL LICENSE DOES NOT INCLUDE
1. The Commercial License does not permit Licensee to:
(a) offer the Licensed Work or the Enterprise Features to third parties as a
hosted service, managed service or software-as-a-service offering (see
the Additional Use Grant in Part A); this requires a separate written
agreement with Licensor;
(b) share, publish, sublicense, rent, resell or otherwise make a License Key
available to anyone outside the Scope;
(c) make production use of the Licensed Work, or use Enterprise Features,
outside the Scope or after the end of the License Term.
2. Licensee's rights in the Licensed Work under Part A - including the rights to
copy, modify, create derivative works and redistribute it - are unaffected by
Part B.
B.5 TERM, RENEWAL AND EXPIRY
1. The License Term starts on the date stated in the Order or, if none is
stated, on activation of the License Key.
2. Renewal is not automatic. To keep the Commercial License, Licensee must
purchase a renewal before the License Term ends.
3. When the License Term ends, or the Commercial License is terminated:
(a) Enterprise Features are disabled automatically;
(b) the Licensed Work continues to function with its remaining features, and
all data, configuration and settings are preserved;
(c) Part A alone governs any further use. A Licensee that is not covered by
clauses (a) to (c) of the Additional Use Grant must therefore renew or
stop production use. The thirty (30) day cure period in the Additional
Use Grant applies after Licensor's written notice.
B.6 FEES AND TAXES
Fees are as stated in the Order, payable as set out in the Order or invoice, and
exclusive of VAT and other applicable taxes, which Licensee pays in addition.
Fees are non-refundable except where required by applicable law or agreed by
Licensor in writing.
B.7 TRANSFER
1. Reassignment. Licensee may move a Commercial License to another installation
within its organization (for example on hardware replacement or migration),
provided the License Key is removed from the previous installation and the
number of active installations stays within the Scope.
2. Business transfers. Licensee may transfer the Commercial License to its legal
successor in a merger, acquisition, reorganization or sale of all or
substantially all of its assets, by written notice to [email protected]
within thirty (30) days of the transfer, stating the successor's legal name and
contact details and confirming that the successor accepts Part A and Part B.
3. No other transfer, resale or sublicensing of a Commercial License or License
Key is permitted without Licensor's prior written consent.
B.8 SUPPORT
1. Licensor provides the support level included in Licensee's tier on a
commercially reasonable, best-effort basis, through [email protected].
2. Unless separately agreed in writing, Licensor does not guarantee response or
resolution times, the availability of updates, patches or new versions, or
compatibility with future operating systems, Docker versions or other software,
and no service level agreement applies.
B.9 DATA AND PRIVACY
The Licensed Work stores all data locally on Licensee's infrastructure and
contains no telemetry or analytics. License Keys are validated locally; the
Licensed Work does not contact Licensor's servers. Licensor does not collect,
access or process data stored in Licensee's installation. Licensee is solely
responsible for the security, backup and protection of that data. Licensor
processes the personal data needed to handle Orders, License Keys and support
requests as described in the Privacy Policy at https://dockhand.pro.
B.10 OWNERSHIP, TRADEMARKS AND FEEDBACK
1. The Licensed Work, including the Enterprise Features, is and remains the
property of Licensor. All rights not expressly granted in Part A or Part B are
reserved.
2. "Dockhand", "Finsys" and the associated logos are trademarks of Licensor.
Neither Part A nor Part B grants any trademark rights beyond those expressly
required by Part A.
3. Licensor may use any feedback, suggestions or enhancement requests Licensee
provides, without obligation to Licensee.
B.11 WARRANTY DISCLAIMER
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK, INCLUDING
THE ENTERPRISE FEATURES, IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. LICENSOR DOES NOT WARRANT THAT
THE LICENSED WORK WILL MEET LICENSEE'S REQUIREMENTS, BE UNINTERRUPTED, TIMELY,
SECURE OR ERROR-FREE, THAT ERRORS WILL BE CORRECTED, OR THAT THE LICENSED WORK
WILL BE COMPATIBLE WITH LICENSEE'S HARDWARE, SOFTWARE OR NETWORK ENVIRONMENT.
THE STATUTORY WARRANTY FOR DEFECTS (RĘKOJMIA) IS EXCLUDED TO THE EXTENT
PERMITTED BY LAW.
LICENSEE ACKNOWLEDGES THAT THE LICENSED WORK IS USED TO MANAGE DOCKER CONTAINERS
AND RELATED INFRASTRUCTURE, AND THAT DEFECTS OR IMPROPER USE MAY RESULT IN DATA
LOSS, DOWNTIME, SYSTEM DAMAGE OR SECURITY INCIDENTS. LICENSEE ASSUMES THAT RISK.
B.12 LIMITATION OF LIABILITY
1. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR IS NOT LIABLE FOR
ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR LOSS
OF PROFITS, REVENUE, DATA, GOODWILL OR USE, BUSINESS INTERRUPTION, SECURITY
BREACHES, DATA CORRUPTION, CONTAINER MANAGEMENT ERRORS, THIRD-PARTY ACTIONS OR
INTEGRATIONS, OR THE COST OF SUBSTITUTE GOODS OR SERVICES, ARISING OUT OF OR IN
CONNECTION WITH PART A, PART B OR THE USE OF OR INABILITY TO USE THE LICENSED
WORK, UNDER ANY THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
2. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR'S TOTAL AGGREGATE
LIABILITY ARISING OUT OF OR IN CONNECTION WITH PART A AND PART B IS LIMITED TO
THE FEES PAID BY LICENSEE UNDER THE ORDER IN THE TWELVE (12) MONTHS PRECEDING THE
EVENT GIVING RISE TO THE CLAIM.
3. Nothing in Part B excludes or limits liability that cannot be excluded or
limited under applicable law, including liability for damage caused
intentionally.
4. These limitations are an essential basis of the bargain between Licensee and
Licensor; Licensor would not offer the Commercial License without them.
B.13 TERMINATION
1. Licensor may terminate the Commercial License by written notice if Licensee
materially breaches Part A or Part B and, where the breach can be cured, fails to
cure it within thirty (30) days of written notice.
2. Licensee may stop using the Commercial License at any time. Fees already paid
are not refunded.
3. On termination, B.5(3) applies. B.6 and B.10 to B.16 survive termination or
expiry.
B.14 GOVERNING LAW AND DISPUTES
Part B, and any dispute arising out of or in connection with it, is governed by
the laws of Poland, excluding its conflict-of-law rules and the UN Convention on
Contracts for the International Sale of Goods. The courts of Warsaw, Poland have
exclusive jurisdiction. Part B is drafted in English; in case of discrepancy
with any translation, the English text prevails.
B.15 GENERAL
1. Changes. Licensor may publish updated Commercial License Terms. Updated terms
apply to Orders placed and renewals purchased after publication. The terms in
force when an Order is placed govern that License Term.
2. Assignment. Licensee may assign Part B only as permitted in B.7. Licensor may
assign Part B to a successor of the Dockhand business on written notice.
3. Severability. If any provision of Part B is unenforceable, it is reformed
only to the extent necessary to make it enforceable, and the remaining
provisions remain in force.
4. Waiver. Licensor's failure to enforce any provision is not a waiver of it.
5. Independent contractors. Nothing in Part B creates a partnership, joint
venture, agency or employment relationship.
6. Export. Licensee will comply with applicable export and import laws in
connection with its use of the Licensed Work.
7. Notices. Notices to Licensor go to [email protected]; notices to
Licensee go to the contact details in the Order.
B.16 LICENSOR
Finsys Jaroslaw Krochmalski
ul. Borki 6, 05-119 Jozefow, Poland
VAT ID: PL7121835977 · REGON: 061576391
[email protected] · https://dockhand.pro
===============================================================================
THIRD-PARTY COMPONENTS
===============================================================================
The Licensed Work incorporates open source components under their own licenses,
including SvelteKit (MIT), Svelte (MIT), Dockerode (Apache 2.0), xterm.js (MIT),
Drizzle ORM (Apache 2.0), Tailwind CSS (MIT), shadcn-svelte (MIT) and Lucide
Icons (ISC). The full list is in the package manifest files. Your use of those
components is governed by their respective licenses; nothing in Part A or Part B
limits your rights under them.
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/Finsys/dockhand/collector
go 1.25.13
go 1.26
+2 -2
View File
@@ -50,12 +50,12 @@
"drizzle-orm": "0.45.2",
"fast-xml-parser": "5.7.3",
"isomorphic-dompurify": "3.22.0",
"js-yaml": "4.3.1",
"js-yaml": "4.3.2",
"ldapts": "8.1.3",
"marked": "18.0.9",
"modern-tar": "0.8.0",
"mqtt": "5.15.2",
"nodemailer": "9.0.1",
"nodemailer": "9.1.0",
"otpauth": "9.4.1",
"postgres": "3.4.8",
"prom-client": "15.1.3",
@@ -0,0 +1,37 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { AlertTriangle } from 'lucide-svelte';
interface Props {
open: boolean;
/** Raw restic stderr for the failed batch (shown verbatim). */
error: string;
/** How many snapshots were still deleted before the failure. */
deleted?: number;
}
let { open = $bindable(), error, deleted = 0 }: Props = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-destructive">
<AlertTriangle class="h-5 w-5" />
Snapshots could not be deleted
</Dialog.Title>
</Dialog.Header>
{#if deleted > 0}
<p class="text-sm text-muted-foreground">
Deleted {deleted} snapshot{deleted === 1 ? '' : 's'}, but the rest failed. The backup
tool reported:
</p>
{:else}
<p class="text-sm text-muted-foreground">The backup tool reported:</p>
{/if}
<pre class="max-h-[50vh] overflow-auto rounded-md bg-muted p-3 text-xs whitespace-pre-wrap break-words font-mono">{error}</pre>
<Dialog.Footer>
<Button onclick={() => (open = false)}>OK</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
+6 -3
View File
@@ -1,8 +1,8 @@
[
{
"version": "1.0.48",
"date": "",
"comingSoon": true,
"date": "2026-09-14",
"comingSoon": false,
"changes": [
{ "type": "feature", "text": "bulk-delete snapshots, and optionally forget a config's snapshots when you delete it (#1556)" },
{ "type": "feature", "text": "option to choose which stack operations show the full log (#1558)" },
@@ -13,7 +13,10 @@
{ "type": "fix", "text": "stop password managers from offering to save stack secret env-var fields (#1517)" },
{ "type": "fix", "text": "repository stats no longer fail on the backups screen (#1561)" },
{ "type": "fix", "text": "dashboard image disk usage no longer counts shared layers multiple times (PR#1461, @omggga)" },
{ "type": "feature", "text": "add a docker attach terminal mode (#1078, PR#1377, @ivanjx)" }
{ "type": "feature", "text": "add a docker attach terminal mode (#1078, PR#1377, @ivanjx)" },
{ "type": "fix", "text": "stack action buttons show for stopped and freshly created stacks, regression (#1565)" },
{ "type": "fix", "text": "the update dialog no longer shows an unreleased version as the target (#1566)" },
{ "type": "fix", "text": "update Go 1.26.8, docker-compose 5.5.0-r5, nodemailer 9.1.0, js-yaml 4.3.2 for security fixes" }
],
"imageTag": "fnsys/dockhand:v1.0.48"
},
+3 -3
View File
@@ -631,7 +631,7 @@
},
{
"name": "dockhand",
"version": "1.0.45",
"version": "1.0.46",
"license": "UNLICENSED",
"repository": null
},
@@ -817,7 +817,7 @@
},
{
"name": "js-yaml",
"version": "4.3.1",
"version": "4.3.2",
"license": "MIT",
"repository": "https://github.com/nodeca/js-yaml"
},
@@ -943,7 +943,7 @@
},
{
"name": "nodemailer",
"version": "9.0.1",
"version": "9.1.0",
"license": "MIT-0",
"repository": "https://github.com/nodemailer/nodemailer"
},
+33
View File
@@ -376,6 +376,39 @@ export function buildNextManifest(newCommit: string, newFiles: Record<string, st
return { commit: newCommit, files: { ...newFiles } };
}
/** Compare two commit refs by their 7-char short form (the DB stores 7-char,
* git returns 40-char); null/empty never matches. */
function sameCommit(a: string | null | undefined, b: string | null | undefined): boolean {
if (!a || !b) return false;
return a.substring(0, 7) === b.substring(0, 7);
}
/**
* Whether a sync can skip re-hashing the whole compose tree. Skipping is safe
* ONLY when the git diff proved no compose-dir change AND the diff was taken
* against the SAME commit the manifest was built at. The second condition is
* load-bearing: a bare Sync advances the diff baseline (lastCommit) without
* persisting the manifest (only a deploy does), so `changedInComposeDir===false`
* alone does NOT imply the on-disk tree still matches the manifest. When the
* baselines match and a prior manifest exists, nothing was added/modified/
* deleted, so the manifest carries forward verbatim and the unconditional
* full re-hash (which blocks the event loop on a large tree) is skipped. Any
* mismatch, undefined change state (new clone / diff failed), or empty manifest
* falls back to a full re-hash - the safe default that self-corrects drift.
*/
export function canSkipTreeRehash(
changedInComposeDir: boolean | undefined,
manifestSize: number,
manifestCommit: string | null | undefined,
diffBaselineCommit: string | null | undefined
): boolean {
return (
changedInComposeDir === false &&
manifestSize > 0 &&
sameCommit(manifestCommit, diffBaselineCommit)
);
}
// =============================================================================
// Sync summary (per-file status table)
// =============================================================================
+37 -6
View File
@@ -34,6 +34,7 @@ import {
hashDirFiles,
computeDeletions,
buildNextManifest,
canSkipTreeRehash,
buildSyncChangeSummary,
formatChangeTable,
skipReasonMessage,
@@ -470,13 +471,34 @@ async function computeSyncDeletionPlan(options: {
composeDir: string; // absolute path inside the clone
composeFileName: string | undefined; // compose file relative to composeDir
rawManifest: string | null | undefined;
// When the caller's git diff proved NOTHING changed in the compose dir between
// the deployed commit and the new clone, hashing the whole tree is pure waste:
// no file changed, so none was deleted. Large trees (tens of thousands of files)
// otherwise block the event loop re-hashing unchanged files on every deploy.
// undefined = unknown -> hash (safe default; e.g. new clone / diff failed).
changedInComposeDir?: boolean;
// The commit the git diff was taken against. Skipping is only safe when this
// equals the manifest's own commit; otherwise the diff baseline has drifted
// from the manifest (e.g. a bare Sync advanced lastCommit without persisting
// the manifest) and a full re-hash is required to stay correct.
diffBaselineCommit?: string | null;
}): Promise<{ plan: DeletionPlan; newFiles: Record<string, string>; previousManifest: SyncManifest }> {
const { logPrefix, composeDir, composeFileName, rawManifest } = options;
const { logPrefix, composeDir, composeFileName, rawManifest, changedInComposeDir, diffBaselineCommit } = options;
const previousManifest = parseManifest(rawManifest);
const manifestSize = Object.keys(previousManifest.files).length;
// Fast path: git proved no compose-dir file changed, the diff baseline matches
// the manifest's commit, and we have a prior manifest. Nothing can have been
// added/modified/deleted, so carry the manifest forward verbatim instead of
// re-hashing the entire tree.
if (canSkipTreeRehash(changedInComposeDir, manifestSize, previousManifest.commit, diffBaselineCommit)) {
console.log(`${logPrefix} Deletion sync: no compose-dir changes, skipping full re-hash of ${manifestSize} file(s)`);
return { plan: { toDelete: [], skipped: [] }, newFiles: { ...previousManifest.files }, previousManifest };
}
const newFiles = hashDirFiles(composeDir);
const manifestSize = Object.keys(previousManifest.files).length;
console.log(`${logPrefix} Deletion sync: manifest has ${manifestSize} file(s)${manifestSize === 0 ? ' (first sync — nothing will be deleted)' : ''}`);
// First sync / legacy manifest: nothing was recorded, so nothing can be deleted
@@ -1236,12 +1258,17 @@ export async function syncGitStack(stackId: number): Promise<SyncResult> {
console.log(`${logPrefix} No env file path configured`);
}
// Deletion sync (#966): manifest-vs-clone deletion plan
// Deletion sync (#966): manifest-vs-clone deletion plan.
// `updated` is the dir-scoped git-diff result (false when the commit didn't
// change OR no compose-dir file changed) - lets the plan skip re-hashing an
// unchanged tree.
const deletionData = await computeSyncDeletionPlan({
logPrefix,
composeDir,
composeFileName,
rawManifest: gitStack.syncedFiles
rawManifest: gitStack.syncedFiles,
changedInComposeDir: updated,
diffBaselineCommit: previousCommit
});
// Update git stack status
@@ -1875,13 +1902,17 @@ export async function deployGitStackWithProgress(
}
}
// Deletion sync (#966): manifest-vs-clone deletion plan
// Deletion sync (#966): manifest-vs-clone deletion plan. `updated` is the
// dir-scoped git-diff result; when nothing changed the plan skips re-hashing
// the whole tree.
const logPrefix = `[Stack:${gitStack.stackName}]`;
const deletionData = await computeSyncDeletionPlan({
logPrefix,
composeDir,
composeFileName: progressComposeFileName,
rawManifest: gitStack.syncedFiles
rawManifest: gitStack.syncedFiles,
changedInComposeDir: updated,
diffBaselineCommit: previousCommit
});
// Update git stack status
+23 -3
View File
@@ -458,6 +458,9 @@ export interface BulkDeleteResult {
skipped: number;
/** true if any destination's batch call returned a non-2xx. */
failed: boolean;
/** Raw restic error from the first failing destination (repo locked, timeout, etc.),
* surfaced verbatim so the user sees the real reason instead of a generic message. */
error?: string;
}
/**
@@ -469,7 +472,7 @@ export interface BulkDeleteResult {
export async function bulkDeleteSnapshots(
targets: { id: string; _destinationId: number }[]
): Promise<BulkDeleteResult> {
let deleted = 0, skipped = 0, failed = false;
let deleted = 0, skipped = 0, failed = false, error: string | undefined;
for (const { destinationId, snapshotIds } of groupSnapshotIdsByDestination(targets)) {
try {
const res = await fetch('/api/backup/snapshots/batch-delete', {
@@ -480,17 +483,34 @@ export async function bulkDeleteSnapshots(
const data = await res.json().catch(() => ({}));
deleted += data.deleted?.length ?? 0;
if (res.ok) skipped += data.skipped?.length ?? 0;
else failed = true;
else {
failed = true;
if (!error && data.error) error = String(data.error);
}
} catch {
failed = true;
}
}
return { deleted, skipped, failed };
return { deleted, skipped, failed, error };
}
/**
* A hard restic failure that warrants the full-error dialog (repo locked, timeout,
* etc.) rather than a toast. Returns the raw restic error and how many were still
* deleted, or null when there is no such failure (toast handles success/skipped).
* A `failed` with no `error` (e.g. a thrown fetch) also returns null - nothing
* verbatim to show - so the caller falls back to the toast.
*/
export function bulkDeleteFailure(r: BulkDeleteResult): { error: string; deleted: number } | null {
if (r.failed && r.error) return { error: r.error, deleted: r.deleted };
return null;
}
/**
* Consistent toast wording for a bulk-delete result, shared by every snapshot list.
* Returns { type, message } so each caller fires its own toast (import stays local).
* A hard restic failure with a verbatim error is handled by the caller via
* bulkDeleteFailure + the error dialog, not here.
*/
export function bulkDeleteToast(r: BulkDeleteResult): { type: 'success' | 'error'; message: string } {
if (r.failed) return { type: 'error', message: r.deleted > 0 ? `Deleted ${r.deleted}, but some snapshots could not be deleted` : 'Snapshots could not be deleted' };
+11
View File
@@ -0,0 +1,11 @@
// Whether the stack row's management actions (edit / logs / start / restart /
// stop) should render. They show for a stack in ANY state EXCEPT a git stack that
// has not been deployed yet - that one shows only its dedicated deploy button, so
// the management actions would be redundant. A non-git stack that is stopped or
// freshly created still needs its Start button, so deploy status must not hide the
// management actions. `status` is the raw stack status; a git stack that has never
// deployed is raw "created" (shown as "not deployed" elsewhere).
export function showsManagementActions(sourceType: string | undefined, status: string): boolean {
return !(sourceType === 'git' && status === 'created');
}
+15 -3
View File
@@ -26,9 +26,10 @@
import { formatDateTime, formatRelativeTime } from '$lib/stores/settings';
import { currentEnvironment } from '$lib/stores/environment';
import { watchJob, readJobResponse } from '$lib/utils/sse-fetch';
import { getRepoTypeIcon, formatCron, retentionSummary, classifyJobResult, tagLogLine, bulkDeleteSnapshots, bulkDeleteToast } from '$lib/utils/backup';
import { getRepoTypeIcon, formatCron, retentionSummary, classifyJobResult, tagLogLine, bulkDeleteSnapshots, bulkDeleteToast, bulkDeleteFailure } from '$lib/utils/backup';
import { Checkbox } from '$lib/components/ui/checkbox';
import BulkDeleteSnapshotsDialog from '$lib/components/BulkDeleteSnapshotsDialog.svelte';
import BulkDeleteErrorDialog from '$lib/components/BulkDeleteErrorDialog.svelte';
import SnapshotBrowser from '../containers/SnapshotBrowser.svelte';
import RestoreModal from '../containers/RestoreModal.svelte';
import BackupLogModal from './BackupLogModal.svelte';
@@ -113,6 +114,9 @@
let bulkDialogOpen = $state(false);
let bulkDeleting = $state(false);
let bulkConfig = $state<BackupConfig | null>(null);
let bulkDeleteErrorOpen = $state(false);
let bulkDeleteError = $state('');
let bulkDeleteErrorDeleted = $state(0);
// Snapshots of a config, normalized to {id, _destinationId} for the shared helper.
function configSelectedTargets(config: BackupConfig) {
return (snapshotsMap.get(config.key) || [])
@@ -139,8 +143,15 @@
bulkDeleting = true;
try {
const result = await bulkDeleteSnapshots(configSelectedTargets(config));
const t = bulkDeleteToast(result);
toast[t.type](t.message);
const failure = bulkDeleteFailure(result);
if (failure) {
bulkDeleteError = failure.error;
bulkDeleteErrorDeleted = failure.deleted;
bulkDeleteErrorOpen = true;
} else {
const t = bulkDeleteToast(result);
toast[t.type](t.message);
}
selectedSnapshots = new Set();
await loadSnapshots(config);
const loaded = snapshotsMap.get(config.key);
@@ -1146,6 +1157,7 @@
</div>
<BulkDeleteSnapshotsDialog bind:open={bulkDialogOpen} count={bulkConfig ? configSelectedTargets(bulkConfig).length : 0} busy={bulkDeleting} onConfirm={bulkDeleteConfig} />
<BulkDeleteErrorDialog bind:open={bulkDeleteErrorOpen} error={bulkDeleteError} deleted={bulkDeleteErrorDeleted} />
<SnapshotBrowser
bind:open={showBrowser}
+15 -3
View File
@@ -8,8 +8,9 @@
import { formatDateTime, formatRelativeTime } from '$lib/stores/settings';
import { formatBytes } from '$lib/utils/format';
import { readJobResponse } from '$lib/utils/sse-fetch';
import { getRepoTypeIcon, selectOwnSnapshotsFromDestination, bulkDeleteSnapshots, bulkDeleteToast } from '$lib/utils/backup';
import { getRepoTypeIcon, selectOwnSnapshotsFromDestination, bulkDeleteSnapshots, bulkDeleteToast, bulkDeleteFailure } from '$lib/utils/backup';
import BulkDeleteSnapshotsDialog from '$lib/components/BulkDeleteSnapshotsDialog.svelte';
import BulkDeleteErrorDialog from '$lib/components/BulkDeleteErrorDialog.svelte';
import SnapshotBrowser from './SnapshotBrowser.svelte';
import RestoreModal from './RestoreModal.svelte';
import SnapshotDiffModal from '../backups/SnapshotDiffModal.svelte';
@@ -57,6 +58,9 @@
let bulkDeleting = $state(false);
let bulkDialogOpen = $state(false);
let bulkScope = $state<'selected' | 'all'>('selected');
let bulkDeleteErrorOpen = $state(false);
let bulkDeleteError = $state('');
let bulkDeleteErrorDeleted = $state(0);
// snapshotId → { filesNew, filesChanged, dataAdded } from schedule executions.
let stats = $state<Map<string, { filesNew: number; filesChanged: number; dataAdded: number }>>(new Map());
@@ -251,8 +255,15 @@
bulkDeleting = true;
try {
const result = await bulkDeleteSnapshots(targets);
const t = bulkDeleteToast(result);
toast[t.type](t.message);
const failure = bulkDeleteFailure(result);
if (failure) {
bulkDeleteError = failure.error;
bulkDeleteErrorDeleted = failure.deleted;
bulkDeleteErrorOpen = true;
} else {
const t = bulkDeleteToast(result);
toast[t.type](t.message);
}
selectedSnapshots = new Set();
await loadSnapshots();
} finally {
@@ -411,6 +422,7 @@
{/if}
<BulkDeleteSnapshotsDialog bind:open={bulkDialogOpen} count={bulkCount} busy={bulkDeleting} onConfirm={() => bulkDelete(bulkScope)} />
<BulkDeleteErrorDialog bind:open={bulkDeleteErrorOpen} error={bulkDeleteError} deleted={bulkDeleteErrorDeleted} />
<SnapshotBrowser
bind:open={showBrowser}
+15 -3
View File
@@ -16,8 +16,9 @@
import { FolderOpen, Box, Layers, FileStack, Camera, ChevronRight, ChevronDown } from 'lucide-svelte';
import { formatBytes } from '$lib/utils/format';
import { Checkbox } from '$lib/components/ui/checkbox';
import { bulkDeleteSnapshots, bulkDeleteToast } from '$lib/utils/backup';
import { bulkDeleteSnapshots, bulkDeleteToast, bulkDeleteFailure } from '$lib/utils/backup';
import BulkDeleteSnapshotsDialog from '$lib/components/BulkDeleteSnapshotsDialog.svelte';
import BulkDeleteErrorDialog from '$lib/components/BulkDeleteErrorDialog.svelte';
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
import { LoadingState } from '$lib/components/ui/loading-state';
import { getRepoTypeIcon, getRepoTypeLabel } from '$lib/utils/backup';
@@ -290,6 +291,9 @@
let selectedSnapshots = $state<Set<string>>(new Set());
let bulkDialogOpen = $state(false);
let bulkDeleting = $state(false);
let bulkDeleteErrorOpen = $state(false);
let bulkDeleteError = $state('');
let bulkDeleteErrorDeleted = $state(0);
function toggleSnapshotSel(id: string) {
const next = new Set(selectedSnapshots);
if (next.has(id)) next.delete(id); else next.add(id);
@@ -310,8 +314,15 @@
bulkDeleting = true;
try {
const result = await bulkDeleteSnapshots(targets);
const t = bulkDeleteToast(result);
toast[t.type](t.message);
const failure = bulkDeleteFailure(result);
if (failure) {
bulkDeleteError = failure.error;
bulkDeleteErrorDeleted = failure.deleted;
bulkDeleteErrorOpen = true;
} else {
const t = bulkDeleteToast(result);
toast[t.type](t.message);
}
selectedSnapshots = new Set();
const dest = destinations.find((d) => d.id === browseDestId);
if (dest) await browseDestination(dest);
@@ -933,6 +944,7 @@
</Dialog.Root>
<BulkDeleteSnapshotsDialog bind:open={bulkDialogOpen} count={browseSelectedCount} busy={bulkDeleting} onConfirm={bulkDeleteBrowsed} />
<BulkDeleteErrorDialog bind:open={bulkDeleteErrorOpen} error={bulkDeleteError} deleted={bulkDeleteErrorDeleted} />
<SnapshotBrowser
bind:open={snapshotBrowseOpen}
+2 -1
View File
@@ -28,6 +28,7 @@
import ContainerIcon from '$lib/components/ContainerIcon.svelte';
import BatchOperationModal from '$lib/components/BatchOperationModal.svelte';
import type { ComposeStackInfo, ContainerStats, StackContainer } from '$lib/types';
import { showsManagementActions } from '$lib/utils/stack-actions';
import StackModal from './StackModal.svelte';
import DeleteStackModal from './DeleteStackModal.svelte';
import ComposeOutputModal from './ComposeOutputModal.svelte';
@@ -2180,7 +2181,7 @@
{/snippet}
</GitDeployProgressPopover>
{/if}
{#if stack.status !== 'not deployed' && stack.status !== 'created'}
{#if showsManagementActions(source.sourceType, stack.status)}
{#if $canAccess('stacks', 'edit')}
{#if source.sourceType === 'git' && source.gitStack}
<button
+39
View File
@@ -0,0 +1,39 @@
import { describe, test, expect } from 'bun:test';
import { canSkipTreeRehash } from '../src/lib/server/git-deletions';
const A = 'aaaaaaa1111111111111111111111111111111111';
const A7 = 'aaaaaaa'; // same 7-char prefix, DB-stored short form
const B = 'bbbbbbb2222222222222222222222222222222222';
describe('canSkipTreeRehash', () => {
test('skips when no change, prior manifest, and baselines match (full vs full)', () => {
expect(canSkipTreeRehash(false, 5, A, A)).toBe(true);
});
test('skips when baselines match by 7-char prefix (manifest full vs DB short)', () => {
expect(canSkipTreeRehash(false, 5, A, A7)).toBe(true);
});
test('does NOT skip when the diff baseline drifted from the manifest commit', () => {
// bare Sync advanced lastCommit to B while manifest stayed at A -> must re-hash
expect(canSkipTreeRehash(false, 5, A, B)).toBe(false);
});
test('does NOT skip when files changed', () => {
expect(canSkipTreeRehash(true, 5, A, A)).toBe(false);
});
test('does NOT skip on the first sync (empty manifest must be built)', () => {
expect(canSkipTreeRehash(false, 0, A, A)).toBe(false);
});
test('does NOT skip when change state is unknown (new clone / diff failed)', () => {
expect(canSkipTreeRehash(undefined, 5, A, A)).toBe(false);
});
test('does NOT skip when either commit is null/empty', () => {
expect(canSkipTreeRehash(false, 5, null, A)).toBe(false);
expect(canSkipTreeRehash(false, 5, A, null)).toBe(false);
expect(canSkipTreeRehash(false, 5, A, '')).toBe(false);
});
});
+26 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, afterEach } from 'bun:test';
import { groupSnapshotIdsByDestination, bulkDeleteSnapshots, bulkDeleteToast } from '../src/lib/utils/backup';
import { groupSnapshotIdsByDestination, bulkDeleteSnapshots, bulkDeleteToast, bulkDeleteFailure } from '../src/lib/utils/backup';
const snap = (id: string, dest: number) => ({ id, _destinationId: dest });
@@ -96,4 +96,29 @@ describe('bulkDeleteSnapshots', () => {
expect(r.failed).toBe(true);
expect(r.deleted).toBe(1);
});
it('carries the raw restic error from the first failing repo', async () => {
globalThis.fetch = (async (_url: string, init: any) => {
const body = JSON.parse(init.body);
if (body.destinationId === 2) return { ok: false, json: async () => ({ deleted: [], error: 'repository is already locked\ncontext canceled' }) } as any;
return { ok: true, json: async () => ({ deleted: ['a'], skipped: [] }) } as any;
}) as any;
const r = await bulkDeleteSnapshots([snap('a', 1), snap('b', 2)]);
expect(r.failed).toBe(true);
expect(r.error).toBe('repository is already locked\ncontext canceled');
});
});
describe('bulkDeleteFailure', () => {
it('returns the raw error and deleted count on a hard failure', () => {
expect(bulkDeleteFailure({ deleted: 1, skipped: 0, failed: true, error: 'repo locked' }))
.toEqual({ error: 'repo locked', deleted: 1 });
});
it('is null when nothing failed (toast handles success/skipped)', () => {
expect(bulkDeleteFailure({ deleted: 3, skipped: 0, failed: false })).toBeNull();
expect(bulkDeleteFailure({ deleted: 0, skipped: 2, failed: false })).toBeNull();
});
it('is null when failed but no verbatim error (thrown fetch) - toast falls back', () => {
expect(bulkDeleteFailure({ deleted: 0, skipped: 0, failed: true })).toBeNull();
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, test, expect } from 'bun:test';
import { showsManagementActions } from '../src/lib/utils/stack-actions';
describe('showsManagementActions', () => {
test('non-git stopped/created stacks show actions (the Start button regression)', () => {
expect(showsManagementActions('internal', 'created')).toBe(true);
expect(showsManagementActions('internal', 'stopped')).toBe(true);
expect(showsManagementActions('external', 'created')).toBe(true);
expect(showsManagementActions(undefined, 'created')).toBe(true);
});
test('git stack that has not been deployed (raw "created") hides actions - only the deploy button shows', () => {
expect(showsManagementActions('git', 'created')).toBe(false);
});
test('a deployed git stack shows its management actions', () => {
expect(showsManagementActions('git', 'running')).toBe(true);
expect(showsManagementActions('git', 'stopped')).toBe(true);
expect(showsManagementActions('git', 'partial')).toBe(true);
});
test('running/stopped stacks of any source show actions', () => {
for (const src of ['internal', 'external', 'git', undefined]) {
expect(showsManagementActions(src, 'running')).toBe(true);
}
});
});