diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 6e16462..e16546d 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -8,7 +8,8 @@ on: tags: - '*' branches: - - '*' + - main + - next jobs: build: @@ -55,6 +56,10 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.CONTAINER_TOKEN }} + - name: get date + run: | + echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV" + - name: make webui run: make -w webui-dist @@ -93,10 +98,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }} GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }} - - name: get date - run: | - echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV" - - name: Archive binaries uses: actions/upload-artifact@v4.3.1 with: diff --git a/AGENTS.md b/AGENTS.md index a64f036..62f10bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - Footer visibility is controlled by `showFooter` from Init API; tests may assert the footer is absent when config disables it. ### Coding Standards (Go) +- Avoid adding superflous comments that explain what the code is doing. Comments are only to describe business logic decisions. - Prefer clear, descriptive names; avoid 1–2 letter identifiers. - Use early returns and handle edge cases first. - Do not swallow errors; propagate or log meaningfully. @@ -56,7 +57,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - Action button behavior: `frontend/resources/vue/ActionButton.vue` ### Contributing Checklist -- Review the contributuing guidelines at `CONTRIBUTING.adoc`. +- Review the contributing guidelines at `CONTRIBUTING.adoc`. - Review the AI guidance in `AI.md`. - Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`. diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 46d7e77..2512305 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -45,10 +45,10 @@ cd OliveTin make githooks # Step3: compile binary for current dev env (OS, ARCH) -# `make grpc` will also run `make go-tools`, which installs "buf". This binary +# `make proto` will also run `make go-tools`, which installs "buf". This binary # will be put in your GOPATH/bin/, which should be on your path. buf is used to -# generate the protobuf / grpc stubs. -make grpc +# generate the protobuf / Connect RPC stubs. +make proto make ./OliveTin ``` @@ -58,7 +58,7 @@ make The project layout is reasonably straightforward; * See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any). -* The API is defined in protobuf+grpc - you will need to `make grpc`. +* The API is defined in protobuf+Connect RPC - you will need to `make proto`. * The Go daemon is built from the `cmd` and `internal` directories mostly. * The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver. diff --git a/Makefile b/Makefile index 7d4cb03..d06002f 100644 --- a/Makefile +++ b/Makefile @@ -17,15 +17,12 @@ it: go-tools: $(MAKE) -wC service go-tools -proto: grpc - -grpc: go-tools +proto: go-tools $(MAKE) -wC proto -dist: protoc +dist: + echo "dist noop" -protoc: - protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. -I .:/usr/include/ OliveTin.proto podman-image: buildah bud -t olivetin @@ -59,4 +56,4 @@ clean: $(call delete-files,reports) $(call delete-files,gen) -.PHONY: grpc proto service +.PHONY: proto service diff --git a/README.md b/README.md index 43c85b0..3bc9214 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app) * **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously. * **Container** - available for quickly testing and getting it up and running, great for the selfhosted community. * **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretically you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin. -* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/gRPC API. +* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/Connect RPC API. * **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability. ## Screenshots diff --git a/frontend/js/ArgumentForm.js b/frontend/js/ArgumentForm.js deleted file mode 100644 index b28f030..0000000 --- a/frontend/js/ArgumentForm.js +++ /dev/null @@ -1,273 +0,0 @@ -class ArgumentForm extends window.HTMLElement { - getQueryParams () { - return new URLSearchParams(window.location.search.substring(1)) - } - - setup (json, callback) { - this.setAttribute('class', 'action-arguments') - - this.constructTemplate() - this.domTitle.innerText = json.title - this.domIcon.innerHTML = json.icon - this.createDomFormArguments(json.arguments) - - this.domBtnStart.onclick = () => { - for (const arg of this.argInputs) { - if (!arg.validity.valid) { - return - } - } - - const argvs = this.getArgumentValues() - - callback(argvs) - - this.remove() - } - - this.domBtnCancel.onclick = () => { - this.clearBookmark() - this.remove() - } - } - - getArgumentValues () { - const ret = [] - - for (const arg of this.argInputs) { - if (arg.type === 'checkbox') { - if (arg.checked) { - arg.value = '1' - } else { - arg.value = '0' - } - } - - if (arg.name === '') { - continue - } - - ret.push({ - name: arg.name, - value: arg.value - }) - } - - return ret - } - - constructTemplate () { - const tpl = document.getElementById('tplArgumentForm') - const content = tpl.content.cloneNode(true) - - this.appendChild(content) - - this.domTitle = this.querySelector('h2') - this.domIcon = this.querySelector('span.icon') - this.domWrapper = this.querySelector('.wrapper') - - this.domArgs = this.querySelector('.arguments') - - this.domBtnStart = this.querySelector('[name=start]') - this.domBtnCancel = this.querySelector('[name=cancel]') - } - - createDomFormArguments (args) { - this.argInputs = [] - - for (const arg of args) { - this.domArgs.appendChild(this.createDomLabel(arg)) - this.domArgs.appendChild(this.createDomSuggestions(arg)) - this.domArgs.appendChild(this.createDomInput(arg)) - this.domArgs.appendChild(this.createDomDescription(arg)) - } - } - - createDomLabel (arg) { - const domLbl = document.createElement('label') - - const lastChar = arg.title.charAt(arg.title.length - 1) - - if (lastChar === '?' || lastChar === '.' || lastChar === ':') { - domLbl.innerHTML = arg.title - } else { - domLbl.innerHTML = arg.title + ':' - } - - domLbl.setAttribute('for', arg.name) - - return domLbl - } - - createDomSuggestions (arg) { - if (typeof arg.suggestions !== 'object' || arg.suggestions.length === 0) { - return document.createElement('span') - } - - const ret = document.createElement('datalist') - ret.setAttribute('id', arg.name + '-choices') - - for (const suggestion of Object.keys(arg.suggestions)) { - const opt = document.createElement('option') - - opt.setAttribute('value', suggestion) - - if (typeof arg.suggestions[suggestion] !== 'undefined' && arg.suggestions[suggestion].length > 0) { - opt.innerText = arg.suggestions[suggestion] - } - - ret.appendChild(opt) - } - - return ret - } - - createDomInput (arg) { - let domEl = null - - if (arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) { - domEl = document.createElement('select') - - // select/choice elements don't get an onchange/validation because theoretically - // the user should only select from a dropdown of valid options. The choices are - // riggeriously checked on StartAction anyway. ValidateArgumentType is only - // meant for showing simple warnings in the UI before running. - - for (const choice of arg.choices) { - domEl.appendChild(this.createSelectOption(choice)) - } - } else { - switch (arg.type) { - case 'html': - domEl = document.createElement('div') - domEl.innerHTML = arg.defaultValue - - return domEl - case 'confirmation': - this.domBtnStart.disabled = true - - domEl = document.createElement('input') - domEl.setAttribute('type', 'checkbox') - domEl.onchange = () => { - this.domBtnStart.disabled = false - domEl.disabled = true - } - break - case 'raw_string_multiline': - domEl = document.createElement('textarea') - domEl.setAttribute('rows', '5') - domEl.style.resize = 'vertical' - break - case 'datetime': - domEl = document.createElement('input') - domEl.setAttribute('type', 'datetime-local') - domEl.setAttribute('step', '1') - break - case 'checkbox': - domEl = document.createElement('input') - domEl.setAttribute('type', 'checkbox') - domEl.setAttribute('name', arg.name) - domEl.setAttribute('value', '1') - - break - case 'password': - case 'email': - domEl = document.createElement('input') - domEl.setAttribute('type', arg.type) - break - default: - domEl = document.createElement('input') - - if (arg.type.startsWith('regex:')) { - domEl.setAttribute('pattern', arg.type.replace('regex:', '')) - } - - domEl.onchange = () => { - this.formatValidation(domEl, arg) - } - } - } - - domEl.name = arg.name - - // Use query parameter value if available - const params = this.getQueryParams() - const paramValue = params.get(arg.name) - - if (paramValue !== null) { - domEl.value = paramValue - } else { - domEl.value = arg.defaultValue - } - - // update the URL when a parameter is changed - domEl.addEventListener('change', this.updateUrlWithArg) - - if (typeof arg.suggestions === 'object' && Object.keys(arg.suggestions).length > 0) { - domEl.setAttribute('list', arg.name + '-choices') - } - - this.argInputs.push(domEl) - - return domEl - } - - async formatValidation (domEl, arg) { - const validateArgumentTypeArgs = { - value: domEl.value, - type: arg.type - } - - const validation = await window.validateArgumentType(validateArgumentTypeArgs) - - if (validation.valid) { - domEl.setCustomValidity('') - } else { - domEl.setCustomValidity(validation.description) - } - } - - updateUrlWithArg (ev) { - if (!ev.target.name) { - return - } - - const url = new URL(window.location.href) - - if (ev.target.type === 'password') { - return - } - - // copy the parameter value - url.searchParams.set(ev.target.name, ev.target.value) - - // Update the URL without reloading the page - window.history.replaceState({}, '', url.toString()) - } - - createDomDescription (arg) { - const domArgumentDescription = document.createElement('span') - domArgumentDescription.classList.add('argument-description') - domArgumentDescription.innerHTML = arg.description - - return domArgumentDescription - } - - createSelectOption (choice) { - const domEl = document.createElement('option') - - domEl.setAttribute('value', choice.value) - domEl.innerText = choice.title - - return domEl - } - - clearBookmark () { - // remove the action from the URL - window.history.replaceState({ - path: window.location.pathname - }, '', window.location.pathname) - } -} - -window.customElements.define('argument-form', ArgumentForm) diff --git a/frontend/js/ExecutionFeedbackButton.js b/frontend/js/ExecutionFeedbackButton.js deleted file mode 100644 index 50e30fb..0000000 --- a/frontend/js/ExecutionFeedbackButton.js +++ /dev/null @@ -1,29 +0,0 @@ -export class ExecutionFeedbackButton extends window.HTMLElement { - onExecutionFinished (LogEntry) { - if (LogEntry.timedOut) { - this.renderExecutionResult('action-timeout', 'Timed out') - } else if (LogEntry.blocked) { - this.renderExecutionResult('action-blocked', 'Blocked!') - } else if (LogEntry.exitCode !== 0) { - this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode) - } else { - this.ellapsed = Math.ceil(new Date(LogEntry.datetimeFinished) - new Date(LogEntry.datetimeStarted)) / 1000 - this.renderExecutionResult('action-success', 'Success!') - } - } - - renderExecutionResult (resultCssClass, temporaryStatusMessage) { - this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']') - this.onExecStatusChanged() - } - - updateDom (resultCssClass, title) { - if (resultCssClass == null) { - this.btn.className = '' - } else { - this.btn.classList.add(resultCssClass) - } - - this.domTitle.innerText = title - } -} diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 0ee97e5..70f5aca 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -4,7 +4,7 @@