Skip to content
Merged

0.25.10 #1131

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7dcd4f9
Add support for protomaps in non-selfhosted mode
Freika Apr 27, 2025
37c95d9
Remove sample points
Freika May 2, 2025
bf4a900
Merge remote-tracking branch 'origin' into feature/pmtiles
Freika May 2, 2025
ba2f39e
Update app version and changelog
Freika May 2, 2025
42e6345
Merge pull request #1129 from Freika/feature/pmtiles
Freika May 2, 2025
4e0143e
Add dokku deployment config
Freika May 2, 2025
a737eed
Merge pull request #1130 from Freika/feature/dokku-deployment
Freika May 2, 2025
5ac61fa
Added credentials for Sidekiq UI
Freika May 2, 2025
7292737
Show datetime with seconds in the Points page.
Freika May 2, 2025
8087229
Fix pmtiles map
Freika May 3, 2025
acf024b
Implement direct upload of import files with progress bar
Freika May 3, 2025
ffc9457
Fix deletion of imports on error
Freika May 3, 2025
8322d92
Update changelog
Freika May 3, 2025
f205b3b
Merge pull request #1134 from Freika/fix/protomaps-leaflet
Freika May 3, 2025
c786671
Refactor points creation to be synchronous
Freika May 3, 2025
ac5d14f
Simply load protomaps-leaflet.js
Freika May 3, 2025
e6fdddd
Fix tests
Freika May 3, 2025
5c49e5f
Merge pull request #1135 from Freika/refactoring/syncronous-points
Freika May 3, 2025
e2d0807
Fix the self-hosted flag
Freika May 4, 2025
20ce80c
feat(build): add oci labels
Saschl May 4, 2025
befe245
Bump strong_migrations from 2.2.0 to 2.3.0
dependabot[bot] May 5, 2025
e9680fd
Update sidekiq credentials
Freika May 8, 2025
ef9a7f5
Merge pull request #1139 from Saschl/build/oci-labels
Freika May 8, 2025
e1f3d18
Merge pull request #1144 from Freika/dependabot/bundler/strong_migrat…
Freika May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .app_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.25.9
0.25.10
7 changes: 7 additions & 0 deletions .github/workflows/build_and_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ jobs:
- name: Install dependencies
run: npm install

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: freikin/dawarich

- name: Login to Docker Hub
uses: docker/login-action@v3.1.0
with:
Expand Down Expand Up @@ -67,6 +73,7 @@ jobs:
file: ./docker/Dockerfile.dev
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,7 @@

/config/credentials/production.key
/config/credentials/production.yml.enc
/config/credentials/staging.key
/config/credentials/staging.yml.enc

Makefile
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

# 0.25.10 - 2025-05-02

## Added

- Vector maps are supported in non-self-hosted mode.
- Credentials for Sidekiq UI are now being set via environment variables: `SIDEKIQ_USERNAME` and `SIDEKIQ_PASSWORD`. Default credentials are `sidekiq` and `password`. If you don't set them, in self-hosted mode, Sidekiq UI will not be protected by basic auth.
- New import page now shows progress of the upload.

## Changed

- Datetime is now being displayed with seconds in the Points page. #1088
- Imported files are now being uploaded via direct uploads.
- `/api/v1/points` endpoint now creates accepted points synchronously.

## Removed

- Sample points are no longer being imported automatically for new users.

# 0.25.9 - 2025-04-29

## Fixed
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
strong_migrations (2.2.0)
strong_migrations (2.3.0)
activerecord (>= 7)
super_diff (0.15.0)
attr_extras (>= 6.2.4)
Expand Down
3 changes: 1 addition & 2 deletions Procfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
web: bundle exec puma -C config/puma.rb

release: bundle exec rails db:migrate
worker: bundle exec sidekiq -C config/sidekiq.yml
13 changes: 12 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
{
"name": "dawarich",
"description": "Dawarich",
"buildpacks": [
{ "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" },
{ "url": "https://github.com/heroku/heroku-buildpack-ruby.git" }
],
"formation": {
"web": {
"quantity": 1
},
"worker": {
"quantity": 0
"quantity": 1
}
},
"scripts": {
"dokku": {
"predeploy": "bundle exec rails db:migrate"
}
}
}
2 changes: 1 addition & 1 deletion app/assets/builds/tailwind.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/controllers/api/v1/points_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def index
end

def create
Points::CreateJob.perform_later(batch_params, current_api_user.id)
points = Points::Create.new(current_api_user, batch_params).call

render json: { message: 'Points are being processed' }
render json: { data: points }
end

def update
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/api/v1/subscriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ def callback

render json: { message: 'Subscription updated successfully' }
rescue JWT::DecodeError => e
Sentry.capture_exception(e)
ExceptionReporter.call(e)
render json: { message: 'Failed to verify subscription update.' }, status: :unauthorized
rescue ArgumentError => e
Sentry.capture_exception(e)
ExceptionReporter.call(e)
render json: { message: 'Invalid subscription data received.' }, status: :unprocessable_entity
end
end
2 changes: 2 additions & 0 deletions app/controllers/exports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def create
rescue StandardError => e
export&.destroy

ExceptionReporter.call(e)

redirect_to exports_url, alert: "Export failed to initiate: #{e.message}", status: :unprocessable_entity
end

Expand Down
58 changes: 46 additions & 12 deletions app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,43 @@ def update
end

def create
files = import_params[:files].reject(&:blank?)
files_params = params.dig(:import, :files)
raw_files = Array(files_params).reject(&:blank?)

files.each do |file|
import = current_user.imports.build(
name: file.original_filename,
source: params[:import][:source]
)
if raw_files.empty?
redirect_to new_import_path, alert: 'No files were selected for upload', status: :unprocessable_entity
return
end

created_imports = []

import.file.attach(io: file, filename: file.original_filename, content_type: file.content_type)
raw_files.each do |item|
next if item.is_a?(ActionDispatch::Http::UploadedFile)

import.save!
import = create_import_from_signed_id(item)
created_imports << import if import.present?
end

redirect_to imports_url, notice: "#{files.size} files are queued to be imported in background", status: :see_other
if created_imports.any?
redirect_to imports_url,
notice: "#{created_imports.size} files are queued to be imported in background",
status: :see_other
else
redirect_to new_import_path,
alert: 'No valid file references were found. Please upload files using the file selector.',
status: :unprocessable_entity
end
rescue StandardError => e
Import.where(user: current_user, name: files.map(&:original_filename)).destroy_all
if created_imports.present?
import_ids = created_imports.map(&:id).compact
Import.where(id: import_ids).destroy_all if import_ids.any?
end

flash.now[:error] = e.message
Rails.logger.error "Import error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
ExceptionReporter.call(e)

redirect_to new_import_path, notice: e.message, status: :unprocessable_entity
redirect_to new_import_path, alert: e.message, status: :unprocessable_entity
end

def destroy
Expand All @@ -68,4 +85,21 @@ def set_import
def import_params
params.require(:import).permit(:source, files: [])
end

def create_import_from_signed_id(signed_id)
Rails.logger.debug "Creating import from signed ID: #{signed_id[0..20]}..."

blob = ActiveStorage::Blob.find_signed(signed_id)

import = current_user.imports.build(
name: blob.filename.to_s,
source: params[:import][:source]
)

import.file.attach(blob)

import.save!

import
end
end
11 changes: 11 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ def human_datetime(datetime)
)
end

def human_datetime_with_seconds(datetime)
return unless datetime

content_tag(
:span,
datetime.strftime('%e %b %Y, %H:%M:%S'),
class: 'tooltip',
data: { tip: datetime.iso8601 }
)
end

def speed_text_color(speed)
return 'text-default' if speed.to_i >= 0

Expand Down
149 changes: 149 additions & 0 deletions app/javascript/controllers/direct_upload_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Controller } from "@hotwired/stimulus"
import { DirectUpload } from "@rails/activestorage"

export default class extends Controller {
static targets = ["input", "progress", "progressBar", "submit", "form"]
static values = {
url: String
}

connect() {
this.inputTarget.addEventListener("change", this.upload.bind(this))

// Add form submission handler to disable the file input
if (this.hasFormTarget) {
this.formTarget.addEventListener("submit", this.onSubmit.bind(this))
}
}

onSubmit(event) {
if (this.isUploading) {
// If still uploading, prevent submission
event.preventDefault()
console.log("Form submission prevented during upload")
return
}

// Disable the file input to prevent it from being submitted with the form
// This ensures only our hidden inputs with signed IDs are submitted
this.inputTarget.disabled = true

// Check if we have any signed IDs
const signedIds = this.element.querySelectorAll('input[name="import[files][]"][type="hidden"]')
if (signedIds.length === 0) {
event.preventDefault()
console.log("No files uploaded yet")
alert("Please select and upload files first")
} else {
console.log(`Submitting form with ${signedIds.length} uploaded files`)
}
}

upload() {
const files = this.inputTarget.files
if (files.length === 0) return

console.log(`Uploading ${files.length} files`)
this.isUploading = true

// Disable submit button during upload
this.submitTarget.disabled = true

// Always remove any existing progress bar to ensure we create a fresh one
if (this.hasProgressTarget) {
this.progressTarget.remove()
}

// Create a wrapper div for better positioning and visibility
const progressWrapper = document.createElement("div")
progressWrapper.className = "mt-4 mb-6 border p-4 rounded-lg bg-gray-50"

// Add a label
const progressLabel = document.createElement("div")
progressLabel.className = "font-medium mb-2 text-gray-700"
progressLabel.textContent = "Upload Progress"
progressWrapper.appendChild(progressLabel)

// Create a new progress container
const progressContainer = document.createElement("div")
progressContainer.setAttribute("data-direct-upload-target", "progress")
progressContainer.className = "w-full bg-gray-200 rounded-full h-4"

// Create the progress bar fill element
const progressBarFill = document.createElement("div")
progressBarFill.setAttribute("data-direct-upload-target", "progressBar")
progressBarFill.className = "bg-blue-600 h-4 rounded-full transition-all duration-300"
progressBarFill.style.width = "0%"

// Add the fill element to the container
progressContainer.appendChild(progressBarFill)
progressWrapper.appendChild(progressContainer)
progressBarFill.dataset.percentageDisplay = "true"

// Add the progress wrapper AFTER the file input field but BEFORE the submit button
this.submitTarget.parentNode.insertBefore(progressWrapper, this.submitTarget)

console.log("Progress bar created and inserted before submit button")

let uploadCount = 0
const totalFiles = files.length

// Clear any existing hidden fields for files
this.element.querySelectorAll('input[name="import[files][]"][type="hidden"]').forEach(el => {
if (el !== this.inputTarget) {
el.remove()
}
});

Array.from(files).forEach(file => {
console.log(`Starting upload for ${file.name}`)
const upload = new DirectUpload(file, this.urlValue, this)
upload.create((error, blob) => {
uploadCount++

if (error) {
console.error("Error uploading file:", error)
} else {
console.log(`Successfully uploaded ${file.name} with ID: ${blob.signed_id}`)

// Create a hidden field with the correct name
const hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("name", "import[files][]")
hiddenField.setAttribute("value", blob.signed_id)
this.element.appendChild(hiddenField)

console.log("Added hidden field with signed ID:", blob.signed_id)
}

// Enable submit button when all uploads are complete
if (uploadCount === totalFiles) {
this.submitTarget.disabled = false
this.isUploading = false
console.log("All uploads completed")
console.log(`Ready to submit with ${this.element.querySelectorAll('input[name="import[files][]"][type="hidden"]').length} files`)
}
})
})
}

directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress", event => {
if (!this.hasProgressBarTarget) {
console.warn("Progress bar target not found")
return
}

const progress = (event.loaded / event.total) * 100
const progressPercentage = `${progress.toFixed(1)}%`
console.log(`Upload progress: ${progressPercentage}`)
this.progressBarTarget.style.width = progressPercentage

// Update text percentage if exists
const percentageDisplay = this.element.querySelector('[data-percentage-display="true"]')
if (percentageDisplay) {
percentageDisplay.textContent = progressPercentage
}
})
}
}
Loading