Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
6f17e38
Split Export model into user and whole-account export
monorkin Dec 12, 2025
8953920
Implement full account exports
monorkin Dec 22, 2025
161efb0
Implement basic imports
monorkin Jan 9, 2026
19ae555
Convert Account::SingleUserExport to User::DataExport
monorkin Jan 9, 2026
e892b74
Simplify settings layout and correct h2 levels
andyra Jan 14, 2026
4f2cb01
Polish up notification settings to match
andyra Jan 14, 2026
00c5f7f
Break import and export objects into record sets
monorkin Jan 16, 2026
3f2a919
Regenerate schemas
monorkin Jan 16, 2026
f457273
Convert double negatives into a positive question
monorkin Jan 16, 2026
122a228
Use convention to create boilerplate record sets
monorkin Jan 19, 2026
60787a0
Touch up export modals
andyra Jan 20, 2026
d55aa25
Polish the session menu page
andyra Jan 20, 2026
ba260db
Nicer file input
andyra Jan 20, 2026
77b1568
Better instructions
andyra Jan 20, 2026
5278b8e
Touch up the Download Export page
andyra Jan 20, 2026
1ca52f0
Smaller border radius to handle long file names
andyra Jan 21, 2026
b97934f
Fix crash when exporting ActiveStorage files
monorkin Jan 26, 2026
0452d24
Fix account creation and import updates
monorkin Jan 29, 2026
2cb77ea
Fix orphaned Entropy records upon Account destruction
monorkin Jan 29, 2026
eb9cfc3
Push updates via Turbo
monorkin Jan 29, 2026
6044303
Remove unused association
monorkin Jan 29, 2026
63f6be4
Resolve import conflicts
monorkin Jan 29, 2026
f50d6d0
Implement cursors for imports
monorkin Jan 29, 2026
e0cee2c
Simplify the import test
monorkin Jan 29, 2026
eed2b3c
Cleanup code
monorkin Jan 29, 2026
635dbb9
Rename the Import controller to Account::Import
monorkin Jan 29, 2026
1841500
Unify ZIP file interactions between all exports
monorkin Jan 30, 2026
992f150
Replace RubyZip with ZipKit
monorkin Jan 30, 2026
b659e32
Name the export ZIPs differently
monorkin Jan 30, 2026
0240ffc
Fix import validation failure
monorkin Jan 30, 2026
27c73ff
Rename validate to check
monorkin Jan 30, 2026
a791fe1
Hide importing accounts
monorkin Jan 30, 2026
774a44f
Tweak email copy
monorkin Jan 30, 2026
dd13a3b
Style
monorkin Jan 30, 2026
5bbd7f6
Process everything in chunks
monorkin Jan 30, 2026
de69d2e
Add import cleanup
monorkin Feb 2, 2026
58a14fb
Fix load error
monorkin Feb 2, 2026
ccbf571
Fix crash on commentable check
monorkin Feb 2, 2026
a6609e1
Add test got the import job
monorkin Feb 2, 2026
2dbbeff
Fix SSL issues when reading remote ZIP files
monorkin Feb 2, 2026
54a7ece
Fail the import if the check fails
monorkin Feb 2, 2026
d7dcf64
Remove model validation
monorkin Feb 2, 2026
8be26a2
Tell brakeman that everything is OK
monorkin Feb 2, 2026
edb676c
Make remote IOs rewindable
monorkin Feb 2, 2026
a70bbf4
Fix crash on account destruction
monorkin Feb 2, 2026
ac7d030
Add size to remote io
monorkin Feb 2, 2026
6e6e4e9
Fix orphaned attachments when deleting the whole account
monorkin Feb 2, 2026
96a1d7d
Fix crash on successful import email
monorkin Feb 2, 2026
60c6327
Fix ActiveStorage writing to disk
monorkin Feb 2, 2026
1bc1b68
Explicitly use the default storage service
monorkin Feb 2, 2026
dca67c5
Bust the cache after import
monorkin Feb 2, 2026
539cd63
Check that associations don't exist
monorkin Feb 2, 2026
b06d95a
Delete accounts for failed imports
monorkin Feb 2, 2026
5dac2fa
Finishing touches
monorkin Feb 2, 2026
5c92923
Rename jobs
monorkin Feb 2, 2026
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 Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ gem "platform_agent"
gem "aws-sdk-s3", require: false
gem "web-push"
gem "net-http-persistent"
gem "rubyzip", require: "zip"
gem "zip_kit"
gem "mittens"
gem "useragent", bc: "useragent"

Expand Down
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.4)
zip_kit (6.3.4)

PLATFORMS
aarch64-linux
Expand Down Expand Up @@ -523,7 +524,6 @@ DEPENDENCIES
rouge
rqrcode
rubocop-rails-omakase
rubyzip
selenium-webdriver
solid_cable (>= 3.0)
solid_cache (~> 1.0)
Expand All @@ -538,6 +538,7 @@ DEPENDENCIES
web-console
web-push
webmock
zip_kit

BUNDLED WITH
2.7.2
3 changes: 2 additions & 1 deletion Gemfile.saas.lock
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ GEM
railties
yabeda (~> 0.8)
zeitwerk (2.7.4)
zip_kit (6.3.4)

PLATFORMS
aarch64-linux
Expand Down Expand Up @@ -678,7 +679,6 @@ DEPENDENCIES
rouge
rqrcode
rubocop-rails-omakase
rubyzip
selenium-webdriver
sentry-rails
sentry-ruby
Expand All @@ -705,6 +705,7 @@ DEPENDENCIES
yabeda-prometheus-mmap
yabeda-puma-plugin
yabeda-rails
zip_kit

BUNDLED WITH
2.7.2
48 changes: 35 additions & 13 deletions app/assets/stylesheets/inputs.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,12 @@
}
}

.input--file {
.input--file,
.input--upload {
cursor: pointer;
display: grid;
inline-size: auto;
place-items: center;

> * {
grid-area: 1 / 1;
}

img {
border-radius: 0.4em;
&:has(input[type="file"]:focus-visible) {
outline: 0.15rem solid var(--color-selected-dark);
}

input[type="file"] {
Expand All @@ -88,10 +82,19 @@
opacity: 0;
}
}
}

&:has(input[type="file"]:focus),
&:has(input[type="file"]:focus-visible) {
outline: 0.15rem solid var(--color-selected-dark);
.input--file {
display: grid;
inline-size: auto;
place-items: center;

> * {
grid-area: 1 / 1;
}

img {
border-radius: 0.4em;
}

&:is(.avatar) {
Expand All @@ -101,6 +104,25 @@
}
}

.input--upload {
--btn-border-color: var(--color-ink);
--btn-border-radius: 1ch;

border-style: dashed;
position: relative;

input[type="file"] {
inset: 0;
outline: none;
position: absolute;
}

&:has([data-upload-preview-target="fileName"]:not([hidden])) {
--btn-border-color: var(--color-positive);
--btn-color: var(--color-positive);
}
}

.input--select {
--input-border-radius: 2em;
--input-padding: 0.5em 1.8em 0.5em 1.2em;
Expand Down
4 changes: 2 additions & 2 deletions app/assets/stylesheets/notifications.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@
display: none;

.notifications--on & {
display: block;
display: inline;
}
}

.notifications__off-message {
display: block;
display: inline;

.notifications--on & {
display: none;
Expand Down
16 changes: 0 additions & 16 deletions app/assets/stylesheets/profile-layout.css

This file was deleted.

23 changes: 20 additions & 3 deletions app/assets/stylesheets/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@
}
}

/* Sections & Panels
/* -------------------------------------------------------------------------- */

.settings__panel {
--panel-size: 100%;
--panel-padding: calc(var(--settings-spacer) / 1);

display: flex;
flex-direction: column;
gap: calc(var(--settings-spacer) / 2);
gap: var(--panel-padding);
min-block-size: 100%;
min-inline-size: 0;

@media (min-width: 960px) {
--panel-padding: calc(var(--settings-spacer) * 1.5) calc(var(--settings-spacer) * 2);
@media (min-width: 640px) {
--panel-padding: calc(var(--settings-spacer) * 2);
}
}

Expand All @@ -38,6 +41,20 @@
}
}

.settings__section {
h2 {
font-size: var(--text-large);
}

> * + * {
margin-block-start: calc(var(--panel-padding) / 2);
}

&:is(:first-child):has(h2) {
margin-top: -0.33lh; /* Align h2 letters caps with panel padding */
}
}

/* Users
/* ------------------------------------------------------------------------ */

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/account/exports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Account::ExportsController < ApplicationController
before_action :ensure_admin_or_owner
before_action :ensure_export_limit_not_exceeded, only: :create
before_action :set_export, only: :show

Expand All @@ -13,8 +14,12 @@ def create
end

private
def ensure_admin_or_owner
head :forbidden unless Current.user.admin? || Current.user.owner?
end

def ensure_export_limit_not_exceeded
head :too_many_requests if Current.user.exports.current.count >= CURRENT_EXPORT_LIMIT
head :too_many_requests if Current.account.exports.current.count >= CURRENT_EXPORT_LIMIT
end

def set_export
Expand Down
44 changes: 44 additions & 0 deletions app/controllers/account/imports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Account::ImportsController < ApplicationController
layout "public"

disallow_account_scope only: %i[ new create ]
allow_unauthorized_access only: :show
before_action :set_import, only: %i[ show ]
before_action :ensure_accessed_by_owner, only: %i[ show ]

def new
end

def create
signup = Signup.new(identity: Current.identity, full_name: "Import", skip_account_seeding: true)

if signup.complete
start_import(signup.account)
else
render :new, alert: "Couldn't create account."
end
end

def show
end

private
def set_import
@import = Current.account.imports.find(params[:id])
end

def ensure_accessed_by_owner
head :forbidden unless @import.identity == Current.identity
end

def start_import(account)
import = nil

Current.set(account: account) do
import = account.imports.create!(identity: Current.identity, file: params[:file])
import.process_later
end

redirect_to account_import_path(import, script_name: account.slug)
end
end
33 changes: 33 additions & 0 deletions app/controllers/users/data_exports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Users::DataExportsController < ApplicationController
before_action :set_user
before_action :ensure_current_user
before_action :ensure_export_limit_not_exceeded, only: :create
before_action :set_export, only: :show

CURRENT_EXPORT_LIMIT = 10

def show
end

def create
@user.data_exports.create!(account: Current.account).build_later
redirect_to @user, notice: "Export started. You'll receive an email when it's ready."
end

private
def set_user
@user = Current.account.users.find(params[:user_id])
end

def ensure_current_user
head :forbidden unless @user == Current.user
end

def ensure_export_limit_not_exceeded
head :too_many_requests if @user.data_exports.current.count >= CURRENT_EXPORT_LIMIT
end

def set_export
@export = @user.data_exports.completed.find_by(id: params[:id])
end
end
27 changes: 22 additions & 5 deletions app/javascript/controllers/upload_preview_controller.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [ "image", "input" ]
static targets = [ "image", "input", "fileName", "placeholder" ]

previewImage() {
const file = this.inputTarget.files[0]

if (file) {
this.imageTarget.src = URL.createObjectURL(file)
if (this.#file) {
this.imageTarget.src = URL.createObjectURL(this.#file)
this.imageTarget.onload = () => URL.revokeObjectURL(this.imageTarget.src)
}
}

previewFileName() {
this.#file ? this.#showFileName() : this.#showPlaceholder()
}

#showFileName() {
this.fileNameTarget.innerHTML = this.#file.name
this.fileNameTarget.removeAttribute("hidden")
this.placeholderTarget.setAttribute("hidden", true)
}

#showPlaceholder() {
this.placeholderTarget.removeAttribute("hidden")
this.fileNameTarget.setAttribute("hidden", true)
}

get #file() {
return this.inputTarget.files[0]
}
}
20 changes: 20 additions & 0 deletions app/jobs/account/data_import_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Account::DataImportJob < ApplicationJob
include ActiveJob::Continuable

queue_as :backend
discard_on Account::DataTransfer::RecordSet::IntegrityError

def perform(import)
step :check do |step|
import.check \
start: step.cursor,
callback: ->(record_set:, file:) { step.set!([ record_set.model.name, file ]) }
end

step :process do |step|
import.process \
start: step.cursor,
callback: ->(record_set:, files:) { step.set!([ record_set.model.name, files.last ]) }
end
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ExportAccountDataJob < ApplicationJob
class DataExportJob < ApplicationJob
queue_as :backend

discard_on ActiveJob::DeserializationError
Expand Down
11 changes: 11 additions & 0 deletions app/mailers/export_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
class ExportMailer < ApplicationMailer
helper_method :export_download_url

def completed(export)
@export = export
@user = export.user

mail to: @user.identity.email_address, subject: "Your Fizzy data export is ready for download"
end

private
def export_download_url(export)
if export.is_a?(User::DataExport)
user_data_export_url(export.user, export)
else
account_export_url(export)
end
end
end
Loading