Compare commits

26 Commits

Author SHA1 Message Date
BenClaw 4a4e076416 fix(action_cable): use Redis adapter in development too
- async adapter is in-process — works in Puma request cycle
  but fails from Rails console (no event loop to deliver messages)
- Redis is shared-state, works from any context (console, jobs, requests)
- Dev uses Redis DB 2, separate channel_prefix from production
2026-05-17 20:16:01 +02:00
BenClaw 02df03282e Revert "fix(broadcasting): broadcast on mark_helped! even when already false"
This reverts commit 4ad701c1a5.
2026-05-17 20:12:22 +02:00
BenClaw 4ad701c1a5 fix(broadcasting): broadcast on mark_helped! even when already false
- mark_helped! gated broadcast on if save, but save returns false
  when needs_help is already false (no dirty attributes in CouchDB)
- Same fix applied to remove_needs_payment!
- Broadcast is the important side effect — save is just persistence
2026-05-17 19:48:19 +02:00
BenClaw bdd1d248db debug(action_cable): add server-side broadcast logging
- Log channel remapping and data on every broadcast
- Log warnings when channel format is unknown
- Helps trace whether broadcasts reach ActionCable.server
2026-05-17 19:41:00 +02:00
BenClaw 7c69f0a0bc fix(action_cable): accept both /user/123 and /user_123 channel formats
- Benjamin standardized on /user_123 in mozo.rb (underscore, no slash)
- Old remap regex ^/user/(.+)$ didn't match /user_123
- Fix: ^/user[/_](.+)$ accepts both separators → user_123
2026-05-17 19:27:30 +02:00
bterkuile 11ba8e7434 channel naming convention change 2026-05-17 12:24:07 -05:00
bterkuile 383872b800 require the action_cable engine instead of just the root 2026-05-17 12:07:15 -05:00
BenClaw ee8861355b fix(action_cable): add missing ApplicationCable::Channel base class
- MozoChannel < ApplicationCable::Channel was failing with
  NameError: uninitialized constant ApplicationCable::Channel
- Standard Rails convention requires both connection.rb and channel.rb
2026-05-17 19:06:00 +02:00
BenClaw df04e99447 fix(action_cable): ensure logger is set for upgraded Rails app
- ActionCable::TaggedLoggerProxy crashes with NoMethodError when
  logger is nil (common in apps upgraded from older Rails)
- Add after_initialize hook to guarantee ActionCable.server.config.logger
  falls back to Rails.logger or
2026-05-17 18:49:13 +02:00
bterkuile 4bee13aae7 activate ActionCable messaging instead of Faye 2026-05-17 11:44:53 -05:00
bterkuile 12836dd14b Switch to Redis counter 2026-05-17 10:48:05 -05:00
bterkuile 5dbb6dbeae Explicitly require action_cable 2026-05-17 09:42:48 -05:00
BenClaw 3e4bcc80c8 feat(counter): add Redis counter adapter, replace DrbCounter
- Add Mozo::Counter::Redis with same get/set/incr/decr interface
- Add redis gem (~> 5.0) to Gemfile
- Update cable.yml to use Redis adapter in production (shared with counters)
- Document DrbCounter → Redis migration in broadcasting-migration.md
- Redis installed and running on vmi3300327
- Leave Faye as current broadcaster; both switches are one-line changes

DrbCounter problems solved:
  - In-memory → persistent (RDB + AOF)
  - Single-process DRb → multi-process safe Redis
  - Atomic INCR/DECR across Puma workers
  - One less custom process to manage
2026-05-17 16:42:09 +02:00
BenClaw a755d8a205 refactor(broadcasting): add Broadcastable to List + Order, remove monkey-patch
- include Broadcastable in app/models/list.rb
- include Broadcastable in app/models/order.rb
- Remove config/initializers/model_broadcast.rb (ApplicationController.new anti-pattern)
- Broadcasting now goes through Mozo.broadcast_* directly, not via controller hack
2026-05-17 16:36:28 +02:00
root 1f52448253 feat(broadcasting): add ActionCable adapter + fix model broadcast anti-pattern
- Add Mozo::Broadcaster::ActionCable as drop-in Faye replacement
- Fix model_broadcast.rb: delegate to Mozo directly instead of
  ApplicationController.new (memory-unsafe anti-pattern)
- Add Broadcastable concern for clean model-side broadcasting
- ActionCable config: async adapter, cable.yml, WebSocket endpoint
- MozoChannel with per-entity authorization (user/supplier/employee)
- Connection auth via auth_token (matches existing auth pattern)
- Mount /cable WebSocket in routes
- Add broadcasting-migration.md with Faye→ActionCable guide
2026-05-17 15:25:49 +02:00
benclaw efccbc2064 docs: add INDEX.md describing mozo repo relationships 2026-05-16 19:41:24 +02:00
bterkuile 7364de2a45 transit 2026-03-28 18:23:35 -05:00
bterkuile 814ac1b808 add explicit environment selector for drb_counter 2026-03-19 08:05:46 -05:00
bterkuile 0559ede912 remove non English locales for easy redesign 2026-03-11 10:43:59 -05:00
bterkuile 4e1d3bd052 Add spanish locales and gravatar options 2026-03-09 11:17:25 -05:00
bterkuile 2711edb167 remove omniauth also from the production check 2026-02-26 08:51:50 -05:00
bterkuile e624ec2d0b User flow updates 2026-02-10 11:38:56 -05:00
bterkuile 63b19cb78a Infrastructure and add removeListNeedsHelp to users 2026-02-09 17:56:05 -05:00
bterkuile be3ee9096b stash apply 2026-02-09 12:52:31 -05:00
bterkuile a1c1a0c34e yodate 2026-02-08 20:00:34 -05:00
bterkuile f0c561311f progress today, clicking and fixing 2026-02-04 15:09:46 -05:00
104 changed files with 1507 additions and 335 deletions
+3 -2
View File
@@ -7,6 +7,7 @@ source 'https://rubygems.org'
gem 'rails', '~> 8.1.1'
#gem 'rails', '7.1.1'
gem 'rack-cors', require: 'rack/cors'
gem 'redis', '~> 5.0'
# Bundle edge Rails instead:
# gem 'rails', git: 'git://github.com/rails/rails.git'
@@ -73,8 +74,8 @@ gem 'simply_stored', github: 'bterkuile/simply_stored', branch: :master
gem 'devise' #, github: 'plataformatec/devise', branch: 'lm-rails-4-2' #, '3.1.0' #, '2.0.4'
gem 'devise_simply_stored', github: 'bterkuile/devise_simply_stored', branch: :master
gem 'devise-i18n'
gem 'omniauth-facebook'
gem 'omniauth-instagram'
#gem 'omniauth-facebook'
#gem 'omniauth-instagram'
#gem 'simple_form'
gem 'active_decorator' #, path: '/Users/bterkuile/companytools/development/rails/components/active_decorator'
#gem 'cmtool', github: 'bterkuile/cmtool'
+37 -69
View File
@@ -10,7 +10,7 @@ GIT
GIT
remote: https://github.com/bterkuile/cmtool.git
revision: 647cc38bad68c3bebc304f52fd54540f4559cae3
revision: 1a1edf675bf25303184977ad809a1d7a57419660
branch: master
specs:
cmtool (3.0.0)
@@ -151,9 +151,9 @@ GEM
airbrussh (1.5.3)
sshkit (>= 1.6.1, != 1.7.0)
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt (3.1.21)
bcrypt_pbkdf (1.1.1)
bigdecimal (3.3.1)
bigdecimal (4.0.1)
builder (3.3.0)
cancancan (3.6.1)
capistrano (3.19.2)
@@ -197,8 +197,8 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
cookiejar (0.3.4)
couchrest (2.0.1)
httpclient (~> 2.8)
@@ -213,11 +213,11 @@ GEM
cucumber-messages (29.0.1)
cuke_modeler (3.26.0)
cucumber-gherkin (< 37.0)
date (3.5.0)
devise (4.9.4)
date (3.5.1)
devise (5.0.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
railties (>= 7.0)
responders
warden (~> 1.2.3)
devise-i18n (1.15.0)
@@ -238,7 +238,7 @@ GEM
eventmachine (>= 1.0.0.beta.4)
email_validator (2.2.4)
activemodel
erb (6.0.0)
erb (6.0.1)
erubi (1.13.1)
eventmachine (1.2.7)
exception_notification (5.0.1)
@@ -250,12 +250,6 @@ GEM
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faraday (2.14.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
faye (1.4.1)
cookiejar (>= 0.3.0)
em-http-request (>= 1.1.6)
@@ -303,15 +297,15 @@ GEM
google-protobuf (>= 3.25, < 5.0)
googleapis-common-protos-types (~> 1.0)
hashdiff (1.2.1)
hashie (5.0.0)
http_parser.rb (0.8.0)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
io-console (0.8.1)
irb (1.15.3)
io-console (0.8.2)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
iso_country_codes (0.7.8)
@@ -324,9 +318,7 @@ GEM
js-routes (2.3.5)
railties (>= 5)
sorbet-runtime
json (2.16.0)
jwt (3.1.2)
base64
json (2.18.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -352,7 +344,7 @@ GEM
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
logger (1.7.0)
loofah (2.24.1)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
@@ -374,16 +366,13 @@ GEM
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (5.26.2)
minitest (6.0.1)
prism (~> 1.5)
momentjs-rails (2.29.4.1)
railties (>= 3.1)
multi_json (1.17.0)
multi_xml (0.7.2)
bigdecimal (~> 3.1)
mutex_m (0.3.0)
naught (1.1.0)
net-http (0.8.0)
uri (>= 0.11.1)
net-imap (0.5.12)
date
net-protocol
@@ -399,32 +388,12 @@ GEM
net-protocol
net-ssh (7.3.0)
nio4r (2.7.5)
nokogiri (1.18.10-aarch64-linux-gnu)
nokogiri (1.19.0-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
oauth2 (2.0.18)
faraday (>= 0.17.3, < 4.0)
jwt (>= 1.0, < 4.0)
logger (~> 1.2)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0, >= 2.0.3)
version_gem (~> 1.1, >= 1.1.9)
omniauth (1.9.2)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-facebook (10.0.0)
bigdecimal
omniauth-oauth2 (>= 1.2, < 3)
omniauth-instagram (1.3.0)
omniauth (~> 1)
omniauth-oauth2 (~> 1)
omniauth-oauth2 (1.7.3)
oauth2 (>= 1.4, < 3)
omniauth (>= 1.9, < 3)
orm_adapter (0.5.0)
ostruct (0.6.3)
paperclip (6.1.0)
@@ -443,6 +412,7 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -451,23 +421,23 @@ GEM
yard (~> 0.9.11)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.6)
psych (5.3.1)
date
stringio
public_suffix (6.0.2)
puma (7.1.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (2.2.21)
rack (3.2.4)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-session (1.0.2)
rack (< 3)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (1.0.1)
rack (< 3)
webrick
rackup (2.3.1)
rack (>= 3)
rails (8.1.1)
actioncable (= 8.1.1)
actionmailbox (= 8.1.1)
@@ -506,10 +476,14 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rake (13.3.1)
rdoc (6.15.1)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
@@ -573,9 +547,6 @@ GEM
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 6.0, != 5.0.0)
snaky_hash (2.0.3)
hashie (>= 0.1.0, < 6)
version_gem (>= 1.1.8, < 3)
sorbet-runtime (0.6.12780)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
@@ -592,13 +563,13 @@ GEM
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
ostruct
stringio (3.1.8)
stringio (3.2.0)
temple (0.10.4)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
test_squad (0.1.3)
rails
thor (1.4.0)
thor (1.5.0)
tilt (2.6.1)
timecop (0.9.10)
timeout (0.4.4)
@@ -612,14 +583,12 @@ GEM
execjs (>= 0.3.0, < 3)
uri (1.1.1)
useragent (0.16.11)
version_gem (1.1.9)
warden (1.2.9)
rack (>= 2.0.9)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
@@ -627,7 +596,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.37)
zeitwerk (2.7.3)
zeitwerk (2.7.4)
PLATFORMS
aarch64-linux
@@ -674,8 +643,6 @@ DEPENDENCIES
mini_magick
mutex_m
naught
omniauth-facebook
omniauth-instagram
pickadate-rails
poltergeist
pry-doc
@@ -684,6 +651,7 @@ DEPENDENCIES
rack-cors
rails (~> 8.1.1)
rails-controller-testing
redis (~> 5.0)
rqrcode
rspec-its
rspec-rails
+83
View File
@@ -0,0 +1,83 @@
# Mozo Project Index
This file is the **canonical source of truth** for how the Mozo repositories relate to each other.
When in doubt about cross-repo conventions, contracts, or terminology, the notes here win.
## Repositories
| Repo | Role | Stack | Notes |
|-----------------------|----------------------------|----------------------|----------------------------------------------------|
| **mozo-backend** | Server / API (this repo) | Ruby on Rails + CouchDB | Main repo. Authoritative data model + API. Owns this INDEX. |
| **mozo-user** | End-user client app | Ember.js (client-side) | Customer-facing app, consumes the backend API. |
| **mozo-supplier** | Supplier client app | Ember.js (client-side) | Supplier-facing app, consumes the backend API. |
| **mozo-employee** *(planned/related)* | Employee client app | Ember.js (client-side) | Internal-staff app, consumes the backend API. |
All client apps speak HTTP(S) to **mozo-backend**. The backend is the single
producer of canonical data; clients are presentation/UX layers around the
same API surface, scoped to their respective user roles.
```
┌────────────────────────────────────────────┐
│ mozo-backend │
│ Rails API · CouchDB · auth · domain │
│ (single source of truth) │
└───────────────▲────────────▲───────────────┘
│ │
┌────────────────────────┘ └────────────────────────┐
│ HTTPS / JSON HTTPS / JSON │
│ │
┌──────────┴──────────┐ ┌──────────────────────┐ ┌────────────────────┴┐
│ mozo-user │ │ mozo-employee │ │ mozo-supplier │
│ Ember client app │ │ Ember client app │ │ Ember client app │
│ (end customers) │ │ (internal staff) │ │ (suppliers) │
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
```
## Source of truth rules
- **Domain model / data model:** defined in `mozo-backend` (`app/models/`, `db/`, `doc/`).
Client apps must follow what the backend exposes; if a client needs new
data, it lands in the backend first.
- **API contracts:** the backend's controllers + serializers are authoritative.
Client repos should not invent endpoints or payload shapes that don't exist
in the backend.
- **Terminology / business rules:** documented in this repo (`doc/`, `wip.md`,
ADRs, this INDEX.md). If a client repo has a conflicting note, the
backend's version wins until it's reconciled here.
- **Auth / sessions:** issued and validated by the backend; clients only
consume tokens/cookies, they don't define the rules.
- **Cross-cutting decisions** (naming, statuses, lifecycle of entities,
deployment flow) belong in this INDEX or in `doc/decisions/` in this repo.
## Client repos (Ember apps) — shared characteristics
- Bootstrapped with Ember CLI (`.ember-cli`, `ember-cli-build.js`,
`testem.js`, `template-lintrc`, `eslint`).
- Each one is a **separate Ember app** targeting a specific audience
(user / supplier / employee), not a monorepo of routes.
- Each app talks to the backend over HTTPS (see backend `config/`,
`expose.sh`, SSL notes in `README.md`).
- Build/deploy is per-client via `deploy.sh` in each client repo; the
backend handles its own deploy via Capistrano (`Capfile`,
`config/deploy*.rb`).
## When you don't know which repo a question belongs in
Ask in this order:
1. Is it about **data, validation, business rules, or the API**? → `mozo-backend`.
2. Is it about **what an end customer sees / does**? → `mozo-user`.
3. Is it about **what a supplier sees / does**? → `mozo-supplier`.
4. Is it about **internal staff tooling / dashboards**? → `mozo-employee`.
5. Still unsure? → default to `mozo-backend` and document the answer here.
## Maintenance
Update this file whenever:
- A new client app or service repo joins the Mozo project.
- A cross-repo convention changes (auth flow, deploy flow, shared naming).
- A repo's role or scope materially shifts.
Keep it short. Detailed design lives in `doc/` and `doc/decisions/`; this file
is a map, not a manual.
+2
View File
@@ -10,6 +10,8 @@ cd ~/projects/couchdb/couchdb
./dev/run --admin=admin:admin
```
Did not work the last time. The docker version did.
### Start rails
Note that the couchdb admin password can be changed for better security:
```
+1
View File
@@ -9,3 +9,4 @@
//= link qr_sheet/application.css
//
// link user/foundation/application.css
//= link admin/application.js
@@ -28,6 +28,12 @@
h3
color: $form-error-color
font-size: 1.2em
// Temporary hack for some forms
.control-group
.controls
.error
// compensate the margin-bottom of the input above
margin-top: -16px
body
label
&.number
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module ApplicationCable
class Connection < ActionCable::Connection::Base
# Authenticate via auth_token (same mechanism used in ApplicationController#authenticate_employee!)
# Clients should pass ?auth_token=TOKEN when connecting to the WebSocket.
identified_by :current_user, :current_entity_type
def connect
token = request.params[:auth_token].presence
reject_unauthorized_connection unless token
if (employee = Employee.find_by_authentication_token(token))
self.current_user = employee
self.current_entity_type = :employee
elsif (user = User.find_by_authentication_token(token))
self.current_user = user
self.current_entity_type = :user
elsif (supplier = Supplier.find_by_authentication_token(token))
self.current_user = supplier
self.current_entity_type = :supplier
else
reject_unauthorized_connection
end
end
# Allow subscribing to the entity's own channel
def subscribe_to_self
case current_entity_type
when :user then "user_#{current_user.id}"
when :supplier then "supplier_#{current_user.id}"
when :employee then "employee_#{current_user.id}"
end
end
end
end
+41
View File
@@ -0,0 +1,41 @@
# frozen_string_literal: true
# Base channel. Streams are set up dynamically by clients subscribing
# to their entity channel (user_123, supplier_456, etc.).
#
# The server broadcasts TO these channels via:
# ActionCable.server.broadcast("user_123", { event: "...", data: {...} })
#
# Clients connect and subscribe via:
# consumer.subscriptions.create({ channel: "MozoChannel", id: "user_123" })
#
class MozoChannel < ApplicationCable::Channel
def subscribed
stream_name = params[:id]
if authorized?(stream_name)
stream_from stream_name
else
reject
end
end
def unsubscribed
# cleanup
end
private
def authorized?(stream_name)
prefix, id = stream_name.to_s.split('_', 2)
case prefix
when 'user'
connection.current_entity_type == :user && connection.current_user.id.to_s == id
when 'supplier'
connection.current_entity_type == :supplier && connection.current_user.id.to_s == id
when 'employee'
connection.current_entity_type == :employee && connection.current_user.id.to_s == id
else
false
end
end
end
@@ -6,10 +6,11 @@ module Admin
end
def new
@svg_element = SvgElement.new
end
def create
@svg_element = SvgElement.new(svg_element_params)
if @svg_element.save
redirect_to [:edit, :admin, @svg_element]
else
@@ -18,6 +19,7 @@ module Admin
end
def update
@svg_element = SvgElement.find(params[:id])
if @svg_element.update_attributes svg_element_params
redirect_to [:edit, :admin, @svg_element]
else
@@ -26,11 +28,11 @@ module Admin
end
def show
@svg_element = SvgElement.find(params[:id])
end
def edit
@svg_element = SvgElement.find(params[:id])
end
private
+21 -6
View File
@@ -13,7 +13,20 @@ class ApplicationController < ActionController::Base
rescue_from SimplyStored::RecordNotFound, with: :show_404
private
# protected
#
# def after_sign_in_path_for(resource)
# case resource
# when 'user' then Mozo.user_url
# else
# main_app.root_path
# end
# # Customize the redirect path here
# # For example, redirect to a dashboard page
# dashboard_path || root_path
# end
#
private
def authenticate_employee!
if auth_token = params[:auth_token].presence || request.headers['HTTP_AUTH_TOKEN'].presence
@@ -67,8 +80,9 @@ private
end
def set_locale
#session[:locale] = (params[:locale].presence || session[:locale] || Rails.configuration.i18n.default_locale).to_sym
I18n.locale = params[:locale].presence.try(:to_sym) || Rails.configuration.i18n.default_locale
session[:locale] = (params[:locale].presence || session[:locale] || Rails.configuration.i18n.default_locale).to_sym
I18n.locale = session[:locale]
# I18n.locale = params[:locale].presence.try(:to_sym) || Rails.configuration.i18n.default_locale
end
def _render_with_renderer_json(resource, options)
@@ -98,9 +112,10 @@ private
def after_sign_in_path_for(resource)
case resource
when Employee then supplier_root_path
when Administrator then cmtool.root_path
else root_path
when User then Mozo.user_url
when Employee then Mozo.supplier_url
when Administrator then cmtool.root_path
else root_path
end
end
+2 -1
View File
@@ -1,9 +1,10 @@
# Contact page form
class ContactFormsController < ApplicationController
def create
@contact_form = Cmtool::ContactForm.new(contact_form_params)
if @contact_form.save
Notifier.contact_form(@contact_form.id).deliver_later
redirect_to root_path, notice: t('contact_form.submitted')
redirect_to root_path, notice: t('website.contact_form.submitted')
else
redirect_to page_path('contact', locale: I18n.locale), alert: @contact_form.errors.full_messages.join(', ')
end
@@ -1,4 +1,4 @@
class Suppliers::SessionsController < Devise::SessionsController
class Employees::SessionsController < Devise::SessionsController
respond_to :json
def create
@@ -10,6 +10,7 @@ class Suppliers::SessionsController < Devise::SessionsController
render json: {employee_id: current_employee.id, auth_token: current_employee.authentication_token}
end
# deprecated?
def destroy
session[:supplier_id] = nil
super
+5
View File
@@ -0,0 +1,5 @@
class ErrorsController < ApplicationController
def not_found
head :not_found # Renders an empty body with 404 status [5]
end
end
@@ -1,13 +0,0 @@
class RegistrationsController < Devise::RegistrationsController
protected
#def after_sign_up_path(resource)
#end
private
# override devise internal to allow name as sign_up param
def sign_up_params
params.require(resource_name).permit resource_class.authentication_keys + [:name, :password, :password_confirmation]
end
end
@@ -114,7 +114,7 @@ module Suppliers
image_type = match[1]
decoded_attribute = Base64.decode64 value.sub BASE64_IMAGE_MATCHER, ''
file = Tempfile.new(['image', ".#{image_type}"])
tempfiles << file
@tempfiles << file
file.binmode
file.write decoded_attribute
authorized_params[attribute] = file
+3
View File
@@ -41,4 +41,7 @@ class UserController < Users::ApplicationController
end
end
def login
end
end
+8 -1
View File
@@ -28,7 +28,6 @@ module Users
orders
orders.product_orders
]
#include_config << 'users' if @list.user_ids.size > 1
render json: @list, include: include_config, serializer: Users::ListSerializer, is_collection: false
end
@@ -60,6 +59,14 @@ module Users
render json: @list
end
# POST /user/remove_list_needs_payment.json
def remove_needs_payment
@list = active_list
render json: json_alert('messages.no_active_list', list_active: false) and return unless @list.try(:id).to_s == params[:id]
@list.remove_needs_payment!
render json: @list
end
# POST /user/lists/:id/move_table.json?table_id=....
# used to move the table
# TODO wrap logic of actions
@@ -0,0 +1,9 @@
class Users::RegistrationsController < Devise::RegistrationsController
def create
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
super
# if resource.persisted?
# self.response_body = nil
# end
end
end
+6
View File
@@ -0,0 +1,6 @@
class UsersController < ApplicationController
def show
authenticate_user!
@user = current_user
end
end
+2 -2
View File
@@ -1,8 +1,8 @@
module ApplicationHelper
# overwrite i18n l, to handle nil values
def l(*args)
def l(*args, **options)
return '' unless args.first
super(*args)
super(*args, **options)
end
def user_root_path
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# Include this in any model that needs to broadcast events to users/suppliers.
#
# Replaces the old model_broadcast.rb initializer which monkey-patched
# SimplyStored::Couch and created ApplicationController.new per broadcast
# (memory-unsafe, no request context, to be removed once all callers migrate).
#
# Usage:
# class List < ApplicationRecord
# include Broadcastable
#
# def close!
# broadcast_user(user.id, 'list_closed', { id: id })
# broadcast_supplier(supplier_id, 'list_closed', { id: id })
# end
# end
#
module Broadcastable
extend ActiveSupport::Concern
def broadcast_supplier(sid, event, data = {})
Mozo.broadcast_supplier(sid, event, data)
end
def broadcast_user(uid, event, data = {})
Mozo.broadcast_user(uid, event, data)
end
end
+4 -1
View File
@@ -19,7 +19,10 @@ class Employee
end
#view :by_confirmation_token, key: :confirmation_token # devise confirmable
devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable #, :registerable #, :confirmable
devise_plugins = [:database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable] #, :omniauthable, {omniauth_providers: [:facebook, :instagram]}] #, :token_authenticatable , :registerable
devise_plugins -= [:trackable] if Rails.env.test? # creates conflicts
devise *devise_plugins
# devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable #, :registerable #, :confirmable
property :unconfirmed_email
property :name
+1
View File
@@ -1,6 +1,7 @@
class List
include SimplyStored::Couch
include ActiveModel::SerializerSupport
include Broadcastable
include List::JoinRequests
per_page_method :limit_value #kaminari
+1
View File
@@ -1,6 +1,7 @@
class Order
include SimplyStored::Couch
include ActiveModel::SerializerSupport
include Broadcastable
property :state, default: 'placed' # placed, active, delivered, cancelled, closed
+5 -1
View File
@@ -38,6 +38,10 @@ class Supplier
property :lat, type: Float #, default: 52.08062426379751
property :lng, type: Float #, default: 4.312562942504883
def suggested_tips
[0, 5, 10, 15, 20]
end
#WIFI
property :offer_wifi
property :wifi_ssid
@@ -215,7 +219,7 @@ class Supplier
private
def add_section_on_create
@section = Section.create supplier: self, title: I18n.t('supplier.section.first_section_title')
@section = Section.create supplier: self, title: I18n.t('supplier.section.first_section_title', default: nil) || 'Room'
end
+7 -1
View File
@@ -6,6 +6,7 @@ class User
property :name
property :active_list_id
property :admin, type: :boolean, default: false
property :email_sha256
#FACEBOOK
property :provider
@@ -14,7 +15,7 @@ class User
property :oauth_expires_at
property :auth_data
devise_plugins = [:database_authenticatable, :recoverable, :rememberable, :trackable, :omniauthable, {omniauth_providers: [:facebook, :instagram]}] #, :token_authenticatable , :registerable
devise_plugins = [:database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable] #, :omniauthable, {omniauth_providers: [:facebook, :instagram]}] #, :token_authenticatable , :registerable
devise_plugins -= [:trackable] if Rails.env.test? # creates conflicts
devise *devise_plugins
@@ -26,6 +27,7 @@ class User
validates_uniqueness_of :email
before_save :ensure_authentication_token
before_create :set_email_sha256
#has_many :error_logs
has_many :user_feedbacks
@@ -150,6 +152,10 @@ class User
reset_authentication_token! if authentication_token.blank?
end
def set_email_sha256
self.email_sha256 = Digest::SHA256.hexdigest email.to_s.strip.downcase
end
def self.authentication_token
SecureRandom.hex(24)
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Suppliers::UserSerializer
include Mozo::SupplierBaseSerializer
attributes :email, :provider, :uid, :avatar, :number_of_lists_at_supplier
attributes :email, :email_sha256, :provider, :uid, :avatar, :number_of_lists_at_supplier
attribute(:name) { object.supplier_name }
end
+1 -1
View File
@@ -1,6 +1,6 @@
class Users::UserSerializer
include Mozo::UserBaseSerializer
attributes :email, :provider, :uid, :avatar
attributes :email, :email_sha256, :provider, :uid, :avatar
attribute(:name){ object.friends_name }
end
+14
View File
@@ -7,5 +7,19 @@ module Mozo
lnd = Lnrpc::Client.new(credentials_path: Rails.application.config.lnd_credentials_path, macaroon_path: Rails.application.config.lnd_macaroon_path)
end
def self.get_info
begin
client.lightning.get_info
rescue StandardError => exception
if exception&.message =~ /wallet locked/
Rails.logger.fatal 'FATAL: LND wallet locked'
#TODO handle wallet locket
end
raise exception
end
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
- model_class = Order
div.page-header= title :index, model_class
- title :index, model_class
- if @orders.any?
table.table.table-striped
thead
@@ -1,5 +1,5 @@
- model_class = ProductCategory
.page-header= title :index, model_class
- title :index, model_class
- if @product_categories.any?
table.table.table-striped
thead
+1 -1
View File
@@ -1,5 +1,5 @@
- model_class = Product
.page-header= title :index, model_class
- title :index, model_class
- if @products.any?
table.table.table-striped
thead
+1 -1
View File
@@ -1,5 +1,5 @@
- model_class = Section
.page-header= title :index, model_class
- title :index, model_class
- if @sections.any?
table.table.table-striped
thead
+15 -7
View File
@@ -1,11 +1,19 @@
= simple_form_for [:admin, @table], html: {class: 'form-horizontal' } do |f|
= form_for [:admin, @table], html: {class: 'form-horizontal' } do |f|
= render 'error_messages', target: @table
= f.input :number
.control-group class=(@table.errors[:supplier_id].any? ? 'error' : nil)
= f.label :supplier_id, Supplier.model_name.human, class: 'control-label'
.controls
= f.select :supplier_id, options_for_select(@suppliers.map{|a| [a.name, a.id]}), include_blank: nil
.form-actions
.form-row
.form-label= f.label :number
.form-field= f.number_field :number
.form-row class=(@table.errors[:supplier_id].any? ? 'error' : nil)
.form-label= f.label :supplier_id, Supplier.model_name.human, class: 'control-label'
.form-field= f.select :supplier_id, options_for_select(@suppliers.map{|a| [a.name, a.id]}), include_blank: nil
/.form-actions
= f.submit nil, class: 'btn btn-primary'
'
= link_to t("helpers.links.cancel"), admin_tables_path, class: 'btn'
/.form-row: .form-actions
= f.submit @submit || update_button_text(f.object), class: 'button'
= link_to t("helpers.links.cancel"), admin_tables_path, class: 'btn'
- content_for :page_links do
ul
li= f.submit @submit || update_button_text(f.object), class: 'button'
li= link_to link_to_index_content(Table), [:admin, Table], class: 'to-index-button'
+2 -2
View File
@@ -1,3 +1,3 @@
- model_class = Table
.page-header= title :edit, model_class
= render 'form'
- title :edit, model_class
= render 'form'
+5 -2
View File
@@ -1,5 +1,5 @@
- model_class = Table
div.page-header= title :index, model_class
- title :index, model_class
- if @tables.any?
table.table.table-striped
thead
@@ -20,5 +20,8 @@ div.page-header= title :index, model_class
= link_to t("helpers.links.destroy"), [:admin, table], method: :delete, data: {confirm: are_you_sure? }, class: 'btn btn-mini btn-danger'
- else
= no_content_given model_class
= link_to t("helpers.links.new"), new_admin_table_path, class: 'btn btn-primary'
/= link_to t("helpers.links.new"), new_admin_table_path, class: 'btn btn-primary'
- content_for :page_links do
ul
li= link_to link_to_new_content(Table), new_admin_table_path, class: 'record-new-button'
+1 -2
View File
@@ -1,4 +1,3 @@
- model_class = Table
.page-header
= title :new, model_class
- title :edit, model_class
= render 'form'
+11 -2
View File
@@ -1,3 +1,12 @@
h1 QR Scan landing page
p |
You are
p
span You scanned a QR code of a
a<> href="https://mozo.bar" mozo.bar
span table.
br
span If you see this page, that means that you did not scan it using the Mozo Web App.
span. If you already have an ccount, please go to the:
a<> href="https://user.mozo.bar/" Mozo User App.
br
span If not, please go to the:
a< href="https://mozo.bar" Mozo User Account Page
@@ -2,6 +2,7 @@ h2= t('devise.registrations.title')
= form_for(resource, :as => resource_name, :url => registration_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
- binding.pry
= f.label :name, class: 'control-label'
.controls= f.text_field :name
.control-group
@@ -15,5 +16,5 @@ h2= t('devise.registrations.title')
.controls= f.password_field :password_confirmation
.control-group
.controls
= f.submit t('devise.registrations.button'), class: 'btn btn-primary'
= render "devise_links"
= f.submit t('devise.registrations.button'), class: 'button2'
= render "devise/links"
@@ -1,29 +0,0 @@
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
<div class="field">
<%= f.label :password %>
<% if @validatable %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
@@ -0,0 +1,11 @@
h2= t('devise.employee.confirmations.title')
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
.control-group
.controls
= f.submit t('devise.employee.confirmations.button'), class: 'button'
= render 'employees/devise/links'
@@ -0,0 +1,23 @@
dl.devise-links
- devise_mapping = Devise.mappings[resource_name]
dt= t 'devise.links.prefix'
- if controller_name != 'sessions'
dd= link_to t('devise.employee.sign_in.link'), new_session_path(resource_name), class: ['devise-link', 'new-session']
- if devise_mapping.registerable? && controller_name != 'registrations'
dd= link_to t('devise.employee.registrations.link'), new_registration_path(resource_name), class: ['devise-link', 'new-registration']
- if resource_name == :employee and controller_name != 'new_suppliers'
dd= link_to t('devise.employee.registrations.link'), new_suppliers_path, class: ['devise-link', 'new-registration']
- if devise_mapping.recoverable? && controller_name != 'passwords'
dd= link_to t('devise.employee.passwords.link'), new_password_path(resource_name), class: ['devise-link', 'forgot-password']
- if devise_mapping.confirmable? && controller_name != 'confirmations'
dd= link_to t('devise.employee.confirmations.did_not_receive_instructions_link'), new_confirmation_path(resource_name), class: ['devise-link', 'did-not-receive-instructions']
- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
dd= link_to t('devise.employee.unlocks.did_not_receive_instructions_link'), new_unlock_path(resource_name), class: ['devise-link', 'did-not-receive-instructions']
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
dd= link_to t('devise.employee.omniauth_callbacks.sign_in_with', provider: provider.to_s.titleize), omniauth_authorize_path(resource_name, provider), class: ['devise-link', 'omniauth', provider]
+12 -10
View File
@@ -1,12 +1,14 @@
h2= t('devise.passwords.edit.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {method: :put}) do |f|
h2= t('devise.employee.passwords.edit.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal', method: :put}) do |f|
= devise_error_messages!
= f.hidden_field :reset_password_token
= f.row :password
.form-label= f.label :password
.form-field= f.password_field :password
= f.row :password_confirmation
.form-label= f.label :password_confirmation
.form-field= f.password_field :password_confirmation
.form-row= f.submit t('devise.passwords.edit.button'), class: 'button'
= render "devise/links"
.control-group
= f.label :password, class: 'control-label'
.controls= f.password_field :password
.control-group
= f.label :password_confirmation, class: 'control-label'
.controls= f.password_field :password_confirmation
.control-group
.controls
= f.submit t('devise.employee.passwords.edit.button'), class: 'button'
= render 'employees/devise/links'
+9 -7
View File
@@ -1,8 +1,10 @@
h2= t('devise.passwords.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name)) do |f|
h2= t('devise.employee.passwords.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
= f.row :email do
.form-label= f.label :email, class: 'control-label'
.form-field= f.email_field :email, autofocus: true
.form-row.form-actions= f.submit t('devise.passwords.button'), class: 'button'
= render "devise/links"
.control-group
= f.label :email, class: 'control-label'
.controls= f.email_field :email
.control-group
.controls
= f.submit t('devise.employee.passwords.button'), class: 'button'
= render 'employees/devise/links'
@@ -0,0 +1,25 @@
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, :autocomplete => "off" %></div>
<div><%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation %></div>
<div><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password %></div>
<div><%= f.submit "Update" %></div>
<% end %>
<h3>Cancel my account</h3>
<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.</p>
<%= link_to "Back", :back %>
@@ -0,0 +1,22 @@
h2= t('devise.employee.registrations.title')
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :name, class: 'control-label'
.controls= f.text_field :name
.control-group
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
- if f.object.errors[:email].present?
small.error= f.object.errors[:email].to_sentence
.control-group
= f.label :password, class: 'control-label'
.controls= f.password_field :password
.control-group
= f.label :password_confirmation, class: 'control-label'
.controls= f.password_field :password_confirmation
.control-group
.controls
= f.submit t('devise.employee.registrations.button'), class: 'button'
= render 'employees/devise/links'
@@ -0,0 +1,25 @@
h2= t('devise.employee.sign_in.title')
= form_for(resource, :as => resource_name, :url => session_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
- if f.object.errors[:email].present?
small.error= f.object.errors[:email].to_sentence
.control-group
= f.label :password, class: 'control-label'
.controls
= f.password_field :password
- if f.object.errors[:password].present?
small.error= f.object.errors[:password].to_sentence
.control-group
.controls
= f.label :remember_me do
= f.check_box :remember_me
|&nbsp;
= t('devise.employee.sign_in.remember_me')
.control-group
.controls
= f.submit t('devise.employee.sign_in.button'), class: 'button'
= render 'employees/devise/links'
+1 -1
View File
@@ -38,4 +38,4 @@
.row
.small-12.columns== @page.footer
= render "devise/links", resource_name: :employee
/= render "devise/links", resource_name: :employee
+1 -1
View File
@@ -16,5 +16,5 @@
.large-8.columns.end= f.email_field :email
.row
.large-12.columns
= f.submit t('contact_form.send_button'), class: 'button'
= f.submit t('website.contact_form.send_button'), class: 'button'
.large-3.columns== @page.sidebar
@@ -1,12 +0,0 @@
<h2>Resend confirmation instructions</h2>
<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.submit "Resend confirmation instructions" %></div>
<% end %>
<%= render "links" %>
@@ -0,0 +1,11 @@
h2= t('devise.user.confirmations.title')
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
.control-group
.controls
= f.submit t('devise.user.confirmations.button'), class: 'button'
= render 'users/devise/links'
+23
View File
@@ -0,0 +1,23 @@
dl.devise-links
- devise_mapping = Devise.mappings[resource_name]
dt= t 'devise.links.prefix'
- if controller_name != 'sessions'
dd= link_to t('devise.user.sign_in.link'), new_session_path(resource_name), class: ['devise-link', 'new-session']
- if devise_mapping.registerable? && controller_name != 'registrations'
dd= link_to t('devise.user.registrations.link'), new_registration_path(resource_name), class: ['devise-link', 'new-registration']
- if resource_name == :employee and controller_name != 'new_suppliers'
dd= link_to t('devise.user.registrations.link'), new_suppliers_path, class: ['devise-link', 'new-registration']
- if devise_mapping.recoverable? && controller_name != 'passwords'
dd= link_to t('devise.user.passwords.link'), new_password_path(resource_name), class: ['devise-link', 'forgot-password']
- if devise_mapping.confirmable? && controller_name != 'confirmations'
dd= link_to t('devise.user.confirmations.did_not_receive_instructions_link'), new_confirmation_path(resource_name), class: ['devise-link', 'did-not-receive-instructions']
- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
dd= link_to t('devise.user.unlocks.did_not_receive_instructions_link'), new_unlock_path(resource_name), class: ['devise-link', 'did-not-receive-instructions']
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
dd= link_to t('devise.user.omniauth_callbacks.sign_in_with', provider: provider.to_s.titleize), omniauth_authorize_path(resource_name, provider), class: ['devise-link', 'omniauth', provider]
@@ -1,6 +1,6 @@
p= t('devise.mailer.confirmation_instructions.salutation', email: @resource.email)
p== t('mailer.user.confirmation_instructions.salutation', email: @resource.email)
p=raw t \
'devise.mailer.confirmation_instructions.body',
p== t \
'mailer.user.confirmation_instructions.body',
unconfirmed_email: @resource.unconfirmed_email,
confirm_url: confirmation_url(@resource, :confirmation_token => @token)
confirm_url: confirmation_url(@resource, confirmation_token: @token)
+3 -3
View File
@@ -1,4 +1,4 @@
h2= t('devise.passwords.edit.title')
h2= t('devise.user.passwords.edit.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal', method: :put}) do |f|
= devise_error_messages!
= f.hidden_field :reset_password_token
@@ -10,5 +10,5 @@ h2= t('devise.passwords.edit.title')
.controls= f.password_field :password_confirmation
.control-group
.controls
= f.submit t('devise.passwords.edit.button'), class: 'btn btn-primary'
= render "links"
= f.submit t('devise.user.passwords.edit.button'), class: 'button'
= render 'users/devise/links'
+3 -3
View File
@@ -1,4 +1,4 @@
h2= t('devise.passwords.title')
h2= t('devise.user.passwords.title')
= form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
@@ -6,5 +6,5 @@ h2= t('devise.passwords.title')
.controls= f.email_field :email
.control-group
.controls
= f.submit t('devise.passwords.button'), class: 'btn btn-primary'
= render "links"
= f.submit t('devise.user.passwords.button'), class: 'button'
= render 'users/devise/links'
+13 -10
View File
@@ -1,19 +1,22 @@
h2= t('devise.registrations.title')
= form_for(resource, :as => resource_name, :url => registration_path(resource_name), html: {class: 'form-horizontal'}) do |f|
h2= t('devise.user.registrations.title')
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :name, class: 'control-label'
.controls= f.text_field :name
.control-group
= f.label :email, class: 'control-label'
.controls= f.email_field :email
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
- if f.object.errors[:email].present?
small.error= f.object.errors[:email].to_sentence
.control-group
= f.label :password, class: 'control-label'
.controls= f.password_field :password
= f.label :password, class: 'control-label'
.controls= f.password_field :password
.control-group
= f.label :password_confirmation, class: 'control-label'
.controls= f.password_field :password_confirmation
= f.label :password_confirmation, class: 'control-label'
.controls= f.password_field :password_confirmation
.control-group
.controls
= f.submit t('devise.registrations.button'), class: 'btn btn-primary'
= render "links"
= f.submit t('devise.user.registrations.button'), class: 'button'
= render 'users/devise/links'
+14 -8
View File
@@ -1,19 +1,25 @@
h2= t('devise.sessions.title')
h2= t('devise.user.sign_in.title')
= form_for(resource, :as => resource_name, :url => session_path(resource_name), html: {class: 'form-horizontal'}) do |f|
= devise_error_messages!
.control-group
= f.label :email, class: 'control-label'
.controls= f.email_field :email
= f.label :email, class: 'control-label'
.controls
= f.email_field :email
- if f.object.errors[:email].present?
small.error= f.object.errors[:email].to_sentence
.control-group
= f.label :password, class: 'control-label'
.controls= f.password_field :password
= f.label :password, class: 'control-label'
.controls
= f.password_field :password
- if f.object.errors[:password].present?
small.error= f.object.errors[:password].to_sentence
.control-group
.controls
= f.label :remember_me do
= f.check_box :remember_me
|&nbsp;
= t('devise.sign_in.remember_me')
= t('devise.user.sign_in.remember_me')
.control-group
.controls
= f.submit t('devise.sign_in.button'), class: 'button'
= render "links"
= f.submit t('devise.user.sign_in.button'), class: 'button'
= render 'users/devise/links'
+5 -2
View File
@@ -5,6 +5,7 @@ require 'rails'
#require 'active_record/railtie'
require 'action_controller/railtie'
require 'action_mailer/railtie'
require 'action_cable/engine'
#require 'active_resource/railtie'
require 'rails/test_unit/railtie'
require 'sprockets/railtie'
@@ -15,6 +16,7 @@ require 'net/http' # lib/mozo/broadcaster/faye.rb
Bundler.require(*Rails.groups(assets: %w[development test user_app]))
Bundler.require(:assets) if ENV['DEPLOY'] == 'yes'
Bundler.require(:test) if ENV['RAILS_TEST'] == 'yes'
#NOTE: the JSON.create_id getter/setter has been moved to Thread.current implementation which
# leads to "json_class" fallbacks for created threads. Maybe this will be fixed for future
@@ -40,7 +42,7 @@ if Rails.env.development?
alias_method :old_execute, :execute
def execute(method, path, options, payload = nil, &block)
Rails.logger.debug "Couch: #{method} #{Rack::Utils.unescape path} #{options}"
puts "Couch: #{method} #{Rack::Utils.unescape path} #{options}"
#puts "Couch: #{method} #{Rack::Utils.unescape path} #{options}"
old_execute(method, path, options, payload, &block)
end
end
@@ -254,7 +256,8 @@ module Mozo
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = :en
config.i18n.available_locales = [:en, :nl]
# config.i18n.available_locales = [:en, :es, :nl]
config.i18n.available_locales = [:en]
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
+19
View File
@@ -0,0 +1,19 @@
# ActionCable configuration for real-time broadcasting.
#
# Development: async adapter (in-process, no external dependency).
# Test: test adapter.
# Production: Redis adapter — required for multi-worker deployments.
# Redis is also used for Mozo::Counter (replacing DrbCounter).
#
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/2" } %>
channel_prefix: mozo_backend_dev
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: mozo_backend
+2
View File
@@ -14,6 +14,8 @@ Mozo::Application.configure do
resource '*', headers: :any, methods: %i[get post put patch delete options]
end
end
#config.action_cable.allowed_request_origins = ['https://localhost:4201', 'https://localhost:4202']
config.lnd_credentials_path = '/mnt/ext1/.lnd/tls.cert'
config.lnd_macaroon_path = '/mnt/ext1/.lnd/data/chain/bitcoin/mainnet/admin.macaroon'
+12
View File
@@ -0,0 +1,12 @@
# frozen_string_literal: true
# Ensure ActionCable has a logger in all environments.
# In apps upgraded from older Rails versions, the logger chain
# may not propagate to ActionCable out of the box, causing:
# NoMethodError (undefined method 'info' for nil)
# in ActionCable::Connection::TaggedLoggerProxy#log
#
Rails.application.config.after_initialize do
ActionCable.server.config.logger ||= Rails.logger || ActiveSupport::Logger.new($stdout)
ActionCable.server.config.logger.level = Rails.logger&.level || Logger::INFO
end
+15 -13
View File
@@ -103,6 +103,7 @@ Devise.setup do |config|
# requests for sign in and sign up, you need to get a new CSRF token
# from the server. You can disable this option at your own risk.
# config.clean_up_csrf_token_on_authentication = true
config.clean_up_csrf_token_on_authentication = false
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 10. If
@@ -240,6 +241,7 @@ Devise.setup do |config|
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html]
config.navigational_formats = ["*/*", :html, :turbo_stream, :json]
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = [:delete, :get]
@@ -248,19 +250,19 @@ Devise.setup do |config|
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
if Rails.env.production?
# config.omniauth :facebook, "505160086210072", "fcc474a3fb13c6bcc0f7c83a92ad1b17",
# scope: 'email,user_birthday,publish_actions'
config.omniauth :facebook, "653729178057509", "d4cea86f70803f1b75ed03c506a4d78e",
scope: 'email,user_birthday,user_gender,user_hometown,user_link,user_location',
provider_ignores_state: true
config.omniauth :instagram, "cd7bdfbee825499b94fb3783d1bc143b", "6b4f9ecf251c462993a696eebc0189be"
else
config.omniauth :facebook, "168928633304849", "22bc53e1a390c1e62d004195c55fe336",
scope: 'email,user_birthday,user_gender,user_hometown,user_link,user_location',
provider_ignores_state: true
config.omniauth :instagram, "81c78b969a7046d6b6b5b5fe3f30929c", "3697c16762ad4f1ca088e829efbaddde"
end
# if Rails.env.production?
# # config.omniauth :facebook, "505160086210072", "fcc474a3fb13c6bcc0f7c83a92ad1b17",
# # scope: 'email,user_birthday,publish_actions'
# config.omniauth :facebook, "653729178057509", "d4cea86f70803f1b75ed03c506a4d78e",
# scope: 'email,user_birthday,user_gender,user_hometown,user_link,user_location',
# provider_ignores_state: true
# config.omniauth :instagram, "cd7bdfbee825499b94fb3783d1bc143b", "6b4f9ecf251c462993a696eebc0189be"
# else
# config.omniauth :facebook, "168928633304849", "22bc53e1a390c1e62d004195c55fe336",
# scope: 'email,user_birthday,user_gender,user_hometown,user_link,user_location',
# provider_ignores_state: true
# config.omniauth :instagram, "81c78b969a7046d6b6b5b5fe3f30929c", "3697c16762ad4f1ca088e829efbaddde"
# end
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
-12
View File
@@ -1,12 +0,0 @@
#TODO: this is really ugly, can cause memory leaks and much more bad stuff. We need a new broadcaster....
require 'simply_stored/couch'
module ModelBroadcast
def broadcast_supplier(*args)
ApplicationController.new.send(:broadcast_supplier, *args)
end
def broadcast_user(*args)
ApplicationController.new.send(:broadcast_user, *args)
end
end
SimplyStored::Couch.send(:include, ModelBroadcast)
#SimplyStored::Couch.send(:extend, ModelBroadcast) # this should never happen!!!
+10 -8
View File
@@ -8,12 +8,14 @@ else
Mozo.user_url = 'https://user.mozo.bar'
end
Mozo.broadcaster = Mozo::Broadcaster::Faye.new
# Broadcaster: swap Faye ↔ ActionCable
# Mozo.broadcaster = Mozo::Broadcaster::Faye.new # current (HTTP POST to Faye)
# Mozo.broadcaster = Mozo::Broadcaster::ActionCable.new # new (in-process async)
#Mozo.broadcaster = Mozo::Broadcaster::Faye.new
Mozo.broadcaster = Mozo::Broadcaster::ActionCable.new # new (in-process async)
# use the connection from couchbase-structures/documents
# will be overwritten in the specs since flushing the real
# thing is difficult
# Mozo::Counter.connection = $cb unless Rails.env.test?
# Use the Drb counter
Mozo::Counter.connection = Mozo::DrbCounter.object unless Rails.env.test?
# Counter: swap DrbCounter ↔ Redis
# Mozo::Counter.connection = Mozo::DrbCounter.object # current (DRb in-memory)
# Mozo::Counter.connection = Mozo::Counter::Redis.new # new (persistent, multi-process)
#Mozo::Counter.connection = Mozo::DrbCounter.object unless Rails.env.test?
Mozo::Counter.connection = Mozo::Counter::Redis.new unless Rails.env.test? # new (persistent, multi-process)
+1 -1
View File
@@ -1,2 +1,2 @@
OmniAuth.config.full_host = Rails.env.production? ? 'https://www.mozo.bar' : 'https://localhost:3002'
#OmniAuth.config.full_host = Rails.env.production? ? 'https://www.mozo.bar' : 'https://localhost:3002'
+41 -15
View File
@@ -1,19 +1,45 @@
en:
devise:
sign_in:
title: Sign in
remember_me: Remember me
link: Sign in
button: Sign in
passwords:
title: Forgot password
link: Forgot password
button: Send reset
edit:
title: Change your password
button: Change password
registrations:
title: Sign up for mozo as a restaurant
link: Sign up
user:
sign_in:
title: Sign in as user
remember_me: Remember me
link: Sign in
button: Sign in
passwords:
title: Recover user password
link: Recover password
button: Send reset
edit:
title: Change your password
button: Change password
registrations:
title: Sign up for mozo as a user
link: Sign up
button: Sign up
confirmations:
title: Resend confirmation instructions for user
button: Resend confirmation instructions
did_not_receive_instructions_link: I did not receive the confirmation e-mail.
employee:
sign_in:
title: Sign in as employee
remember_me: Remember me
link: Sign in
button: Sign in
passwords:
title: Recover password for employee
link: Recover password
button: Send reset
edit:
title: Change employee password
button: Change password
registrations:
title: Sign up for mozo as a employee
link: Sign up
button: Sign up
confirmations:
title: Resend confirmation instructions for employee
button: Resend confirmation instructions
links:
prefix: 'OR:'
+45
View File
@@ -0,0 +1,45 @@
es:
devise:
user:
sign_in:
title: Iniciar sesión como usuario
remember_me: Recordarme
link: Iniciar sesión
button: Iniciar sesión
passwords:
title: Recuperar contraseña de usuario
link: Recuperar contraseña
button: Enviar reinicio
edit:
title: Cambiar tu contraseña
button: Cambiar contraseña
registrations:
title: Regístrate en mozo como usuario
link: Registrarse
button: Registrarse
confirmations:
title: Reenviar instrucciones de confirmación para usuario
button: Reenviar instrucciones de confirmación
did_not_receive_instructions_link: No recibí el correo con instrucciones de confirmar
employee:
sign_in:
title: Iniciar sesión como empleado
remember_me: Recordarme
link: Iniciar sesión
button: Iniciar sesión
passwords:
title: Recuperar contraseña de empleado
link: Recuperar contraseña
button: Enviar reinicio
edit:
title: Cambiar contraseña de empleado
button: Cambiar contraseña
registrations:
title: Regístrate en mozo como empleado
link: Registrarse
button: Registrarse
confirmations:
title: Reenviar instrucciones de confirmación para empleado
button: Reenviar instrucciones de confirmación
links:
prefix: 'O:'
+42 -16
View File
@@ -1,19 +1,45 @@
nl:
devise:
sign_in:
title: Inloggen
remember_me: Mij onthouden
link: Inloggen
button: Inloggen
passwords:
title: Wachtwoord vergeten
link: Wachtwoord vergeten
button: Stuur reset
edit:
title: Change your password
button: Change password
registrations:
title: Aanmelden als restaurant
link: Aanmelden
user:
sign_in:
title: Sign in as user
remember_me: Remember me
link: Sign in
button: Sign in
passwords:
title: Recover user password
link: Recover password
button: Send reset
edit:
title: Change your password
button: Change password
registrations:
title: Sign up for mozo as a user
link: Sign up
button: Sign up
confirmations:
title: Resend confirmation instructions for user
button: Resend confirmation instructions
did_not_receive_instructions_link: I did not receive the confirmation e-mail.
employee:
sign_in:
title: Sign in as employee
remember_me: Remember me
link: Sign in
button: Sign in
passwords:
title: Recover password for employee
link: Recover password
button: Send reset
edit:
title: Change employee password
button: Change password
registrations:
title: Sign up for mozo as a employee
link: Sign up
button: Sign up
confirmations:
title: Resend confirmation instructions for employee
button: Resend confirmation instructions
links:
prefix: 'OF:'
prefix: 'OR:'
+70
View File
@@ -0,0 +1,70 @@
es:
hello: "Hola mundo"
page:
not_found: "404 La página que buscaste no se encontró"
helpers:
links:
are_you_sure: '¿Estás seguro?'
place_order: Realizar pedido de %{models.order}
show_active_list: Mostrar %{list}
edit: Editar
show: Mostrar
new: Nuevo
destroy: Eliminar
back: Volver
cancel: Cancelar
index: Resumen
forms:
errors:
title: Se encontraron problemas al guardar (%{count})
submit:
create: 'Agregar %{model}'
update: 'Actualizar %{model}'
submit: 'Guardar %{model}'
list:
no_records: No hay elementos presentes
actions:
title: Acciones
messages:
cannot_order_on_non_active_list: No puedes realizar un %{models.order} en una %{models.list} cerrada
no_active_list: No hay una %{models.list} activa
order_is_placed: Tu pedido ha sido recibido correctamente
new_list_created: Se ha creado una nueva %{models.list}
the_list_has_been_closed: La %{models.list} ha sido cerrada
illegal_history_list_attempt: La %{models.list} que intentas acceder no es tuya
table_not_found: La %{models.table} solicitada no se encontró o no fue proporcionada
table_is_occupied: La %{models.table} en la que quieres sentarte ya está ocupada
table_is_reserved: La %{models.table} que quieres ocupar está reservada por otra persona
table_is_closed: La %{models.table} en la que quieres sentarte no está disponible para atender
supplier_is_closed: El propietario de esta %{models.table} actualmente no está atendiendo pedidos
join_request_rejected: Tu solicitud para unirte a la %{models.table} ha sido rechazada
join_request_approved: Tu solicitud para unirte a la %{models.table} ha sido aprobada
table_is_from_other_supplier: No puedes moverte a otra %{models.list} cuando tienes una %{models.list} abierta
moved_to_another_table: Te has movido exitosamente a otra %{models.table}
cannot_identify_table: La aplicación no puede determinar el %{models.table} %{attributes.table.number}
action:
index:
label: Listado de %{models}
new:
label: Nuevo %{model}
show:
label: Mostrando %{model}
edit:
label: Editar %{model}
create:
successfull: '%{model} se ha creado exitosamente'
update:
successfull: '%{model} se ha actualizado exitosamente'
destroy:
successfull: '%{model} se ha eliminado exitosamente'
table:
is_occupied: Esta %{models.table} está ocupada
general:
boolean:
boolean_yes: "Sí"
boolean_no: "No"
selected_products:
clear: Limpiar
order: Pedir
product_variant:
add_product_variant: Agregar %{models.product_variant}
+12
View File
@@ -0,0 +1,12 @@
en:
mailer:
supplier:
creation:
subject: 'Weklom bij mozo.bar'
title: 'De aanmelding van %{name} bij mozo.bar is gelukt!'
user:
confirmation_instructions:
salutation: Welcome to mozo.bar. You registered a new account as %{email}
body: |
Your registration at the moment is unconfirmed. To confirm your email, click on the following link:
<a href="%{confirm_url}">Confirm %{unconfirmed_email}</a>
+12
View File
@@ -0,0 +1,12 @@
es:
mailer:
supplier:
creation:
subject: 'Bienvenido a mozo.bar'
title: '¡El registro de %{name} en mozo.bar ha sido exitoso!'
user:
confirmation_instructions:
salutation: Bienvenido a mozo.bar. Te registraste con el correo %{email}
body: |
Tu registro actualmente no está confirmado. Para confirmar tu correo, haz clic en el siguiente enlace:
<a href="%{confirm_url}">Confirmar %{unconfirmed_email}</a>
+6
View File
@@ -4,3 +4,9 @@ nl:
creation:
subject: 'Weklom bij mozo.bar'
title: 'De aanmelding van %{name} bij mozo.bar is gelukt!'
user:
confirmation_instructions:
salutation: Welcome to mozo.bar. You registered a new account as %{email}
body: |
Your registration at the moment is unconfirmed. To confirm your email, click on the following link:
<a href="%{confirm_url}">Confirm %{unconfirmed_email}>
+136
View File
@@ -0,0 +1,136 @@
es:
activemodel:
models:
user: Usuario
supplier: Restaurante
new_supplier: Nuevo restaurante
table: Mesa
list: Cuenta
product: Producto
order: Pedido
product_category: Categoría de producto
product_variant: Variante
section: Sección
join_request: Solicitud de unión
user_feedback: Comentario de usuario
employee: Empleado
employee_shift: Turno
svg_element: Elemento SVG
section_element: Elemento de sección
section_area: Área de sección
plural:
user: Usuarios
supplier: Restaurantes
new_supplier: Nuevos restaurantes
table: Mesas
list: Cuentas
product: Productos
order: Pedidos
product_category: Categorías de producto
product_variant: Variantes
section: Secciones
join_request: Solicitudes de unión
user_feedback: Comentarios de usuario
employee: Empleados
employee_shift: Turnos
svg_element: Elementos SVG
section_element: Elementos de sección
section_area: Áreas de sección
attributes:
product_category:
name: Nombre
position: Posición
week_days: Disponibilidad
full_day: Todo el día
start_from: Desde
end_on: Hasta
visible_on: Activo el
product:
name: Nombre
code: Código
price: Precio
description: Descripción
active: "¿Activo?"
visible: "¿Visible?"
created_at: Creado
image: Imagen
product_variant:
name: Nombre
list:
created_at: Creado
state: Estado
needs_help: Necesita atención
needs_payment: Quiere pagar
closed_at: Cerrado a las
price: Total
section:
title: Título
width: Ancho
height: Largo
created_at: Creado
supplier:
name: Nombre del %{models.supplier}
user_message: Mensaje para %{models.plural.user}
email: 'Correo electrónico'
password: 'Contraseña'
password_confirmation: 'Confirmación'
location: Ubicación
time_zone: Zona horaria
iens_profile: ID de perfil Iens
address: Dirección
postal_code: Código postal
city: Ciudad
country: País
new_supplier:
supplier_name: Nombre del restaurante
email: Correo electrónico
password: Contraseña
password_confirmation: Confirmación de contraseña
table:
table_number: Número
from_number: Desde número
to_number: Hasta número
created_at: Creado
width: Ancho
height: Alto
user:
name: Nombre
email: Correo electrónico
password: 'Contraseña'
password_confirmation: 'Confirmación'
employee:
name: Nombre
email: Correo electrónico
manager: '¿Gerente?'
active: '¿Activo?'
color: Color
employee_shift:
description: Descripción
cmtool/contact_form:
name: "Tu nombre:"
body: 'Tu pregunta o comentarios:'
email: "Tu correo electrónico:*"
male: "Sr."
female: "Sra."
svg_element:
name: Nombre
svg: SVG
dpm: Puntos por metro
box_width: Ancho de caja
box_height: Alto de caja
snap_code: Código QR
section_element:
name: Nombre
svg: SVG
dpm: Puntos por metro
box_width: Ancho de caja
box_height: Alto de caja
snap_code: Código QR
position_x: X
position_y: Y
rotation: Ángulo
section_area:
title: Título
width: Ancho
height: Alto
rounded: "¿Redondeado?"
+9
View File
@@ -0,0 +1,9 @@
es:
simple_form:
"yes": 'Sí'
"no": 'No'
required:
text: 'requerido'
mark: '*'
error_notification:
default_message: "Por favor revisa los problemas a continuación:"
+13
View File
@@ -0,0 +1,13 @@
es:
site:
home:
introduction: >
Bienvenido a la página de mozo.bar. Mozo.bar es una aplicación que te permite hacer pedidos
simplemente escaneando un código de una mesa en una terraza o restaurante. Entonces,
directamente aparece el menú y puedes hacer tu pedido. ¡Mira el progreso mientras esperas tus bebidas!
development: >
Actualmente mozo.bar está en fase de desarrollo. Esto significa que todavía hay mucho por hacer y que
estamos totalmente abiertos a toda la información que podamos recibir. Nuestra misión es complacer tanto al cliente como
al propietario del bar con las posibilidades que nos ofrece esta época.
enroll:
Inscríbete en %{facebook} o %{twitter} para mantenerte informado.
+9
View File
@@ -0,0 +1,9 @@
es:
new_supplier:
already_signed_in_new_restaurant_button_text: "Agregar un nuevo %{supplier} contigo como primer gerente"
employee_already_signed_in: |
Ya has iniciado sesión como %{employee}, por lo que hay algunas acciones posibles:
<ol>
<li><a href="/supplier" class="button">Ir a la aplicación para gestionar los pedidos</a></li>
<li>%{new_supplier_button}</li>
</ol>
+6
View File
@@ -0,0 +1,6 @@
es:
waiter:
product_orders:
order_button: Pedir
total: Total
no_orders: Sin productos
+5
View File
@@ -0,0 +1,5 @@
es:
website:
contact_form:
submitted: Has enviado el formulario de contacto exitosamente
send_button: "Enviar formulario"
+17 -5
View File
@@ -1,13 +1,23 @@
ALLOWED_LOCALES = /nl|de|fr|en|es/
Mozo::Application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
# ActionCable WebSocket endpoint (replaces Faye at events.mozo.bar/faye)
# Clients connect via: wss://mozo.bar/cable?auth_token=TOKEN
mount ActionCable.server => '/cable'
match '/.well-known/*rest', to: 'errors#not_found', via: :all
match '/system/*rest', to: 'errors#not_found', via: :all
devise_for :users, controllers: {
registrations: 'users/registrations',
} #, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
resources :users, only: [:show]
#devise_for :suppliers, controllers: { confirmations: 'confirmations', registrations: 'registrations' }
devise_for :employees, controllers: {
#confirmations: 'confirmations',
#registrations: 'registrations',
sessions: 'suppliers/sessions'
sessions: 'employees/sessions'
}
devise_for :administrators
namespace :admin do
resources :users do
collection do
@@ -35,9 +45,6 @@ Mozo::Application.routes.draw do
get 'empty-page' => 'dashboard#empty_page', as: :empty_page
post '/user_app' => 'dashboard#user_app_log' #TODO: separate high speed app at log.mozo.bar
post '/user_feedback' => 'user#feedback'
#WAITER
#get '/waiter' => 'waiter#index' #, controller: 'waiter', action: 'index'
#get '/waiter/sections' => 'waiter#sections'
@@ -73,7 +80,11 @@ Mozo::Application.routes.draw do
#post '/user/reject_join_request' => 'user#reject_join_request'
#post '/user/approve_join_request' => 'user#approve_join_request'
post '/user_app' => 'dashboard#user_app_log' #TODO: separate high speed app at log.mozo.bar
post '/user_feedback' => 'user#feedback'
get '/user/obtain_token' => 'user#obtain_token', as: :user_obtain_token
post '/user/login' => 'user#login', as: :user_login
#post '/user/obtain_token' => 'user#obtain_token', constraints: {format: :json}
get '/close_window' => 'dashboard#close_window'
namespace :users, path: '/user/api/v1' do
@@ -92,6 +103,7 @@ Mozo::Application.routes.draw do
get :orders
get :users
post :needs_payment
post :remove_needs_payment
post :move_to_table
post :order_products
post :reject_join_request
+149
View File
@@ -0,0 +1,149 @@
# Broadcasting: Faye → ActionCable Migration Guide
## Current state
```
Model (SimplyStored::Couch)
→ ApplicationController.new.broadcast_user # ⚠️ anti-pattern
→ Mozo.broadcast_user
→ Mozo::Broadcaster::Faye.new.broadcast # HTTP POST to Faye
→ Faye server (Thin, port 9296)
→ WebSocket → browser clients
```
## Target state
```
Model (Broadcastable concern)
→ Mozo.broadcast_user
→ Mozo::Broadcaster::ActionCable.new.broadcast # in-process async
→ ActionCable (Rails built-in)
→ WebSocket → browser clients
```
## What this branch adds
| File | Purpose |
|------|---------|
| `lib/mozo/broadcaster/action_cable.rb` | Drop-in ActionCable broadcaster adapter |
| `config/cable.yml` | ActionCable configuration (async for single-server) |
| `app/channels/application_cable/connection.rb` | WebSocket auth via auth_token |
| `app/channels/mozo_channel.rb` | Channel authorization for user/supplier/employee |
| `app/models/concerns/broadcastable.rb` | Clean module for models (replaces old monkey-patch) |
| `config/routes.rb` | Mounts `/cable` WebSocket endpoint |
| `config/initializers/model_broadcast.rb` | Fixed: delegates to Mozo directly (no more `ApplicationController.new`) |
## How to switch
### 1. Server (one-line change)
In `config/initializers/mozo_settings.rb`, change:
```ruby
# Mozo.broadcaster = Mozo::Broadcaster::Faye.new # old
Mozo.broadcaster = Mozo::Broadcaster::ActionCable.new # new
```
### 2. Client (mozo-user / mozo-supplier)
**Old Faye client (conceptual):**
```js
var client = new Faye.Client('https://events.mozo.bar/faye');
client.subscribe('/user/123', function(msg) { ... });
```
**New ActionCable client:**
```js
// Using @rails/actioncable npm package
import { createConsumer } from "@rails/actioncable";
const consumer = createConsumer(
`wss://mozo.bar/cable?auth_token=${authToken}`
);
consumer.subscriptions.create(
{ channel: "MozoChannel", id: "user_123" },
{
received(data) {
// data = { event: "list_closed", data: { id: 42 } }
handleEvent(data.event, data.data);
}
}
);
```
### 3. Remove Faye
Once stable:
- Remove `gem 'faye'` from Gemfile
- Remove `faye/` directory
- Remove nginx `events.mozo.bar` vhost
- Stop the Faye Thin process
## Benefits
- **No extra process** — ActionCable runs inside Puma
- **Async**`broadcast` is non-blocking
- **Simpler deploys** — one less service to manage
- **WebSocket native** — no long-polling fallback complexity
- **Rails auth** — cookies/sessions work automatically
---
# Counter: DrbCounter → Redis Migration
## Current state
```
Supplier::Counters (app/models/supplier/counters.rb)
→ Mozo::Counter.incr/decr/get/set
→ Mozo::Counter.connection (Mozo::DrbCounter.object)
→ DRb → druby://localhost:9022
→ InMemoryQCounter (separate Ruby process)
→ on startup: reloads counts from CouchDB
→ in-memory only (lost on restart)
```
## Problems
1. **In-memory only** — restart the DRb process = lose all counts until CouchDB reload
2. **Single-process** — DRb runs one Ruby process, single point of failure
3. **Separate process** — another thing to monitor, deploy, and restart
4. **Race conditions** — between Puma workers, increment/decrement is not atomic across the DRb boundary
5. **Custom code**`InMemoryQCounter` is 100 lines of hand-rolled counter logic
## Target state
```
Supplier::Counters
→ Mozo::Counter.incr/decr/get/set
→ Mozo::Counter.connection (Mozo::Counter::Redis.new)
→ Redis (localhost:6379)
→ persistent, atomic, multi-process safe
```
## How to switch
In `config/initializers/mozo_settings.rb`, change:
```ruby
# Mozo::Counter.connection = Mozo::DrbCounter.object # old
Mozo::Counter.connection = Mozo::Counter::Redis.new # new
```
That's it. All existing `Mozo::Counter.get/set/incr/decr` calls work unchanged.
## What Redis provides
- **Atomic INCR/DECR** — no race conditions
- **Persistence** — RDB snapshots + AOF, survives restarts
- **Multi-process** — all Puma workers share the same Redis
- **Already needed** — ActionCable uses Redis for pub/sub in production
- **Battle-tested** — millions of deployments
## Migration steps
1. `apt-get install redis-server` — already done on vmi3300327
2. `gem 'redis', '~> 5.0'` — added to Gemfile
3. Switch `Mozo::Counter.connection` — one-line change in mozo_settings.rb
4. Stop the DRb counter process (`drb_counter/drb_counter.rb`)
+1 -1
View File
@@ -1,4 +1,4 @@
source 'https://rubygems.org'
gem "couchrest"
gem "pry"
#gem "pry"
+10 -12
View File
@@ -1,27 +1,25 @@
GEM
remote: https://rubygems.org/
specs:
coderay (1.1.2)
couchrest (2.0.1)
httpclient (~> 2.8)
mime-types (>= 1.15)
multi_json (~> 1.7)
httpclient (2.8.3)
method_source (0.9.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
multi_json (1.14.1)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
httpclient (2.9.0)
mutex_m
logger (1.7.0)
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0203)
multi_json (1.19.1)
mutex_m (0.3.0)
PLATFORMS
ruby
DEPENDENCIES
couchrest
pry
BUNDLED WITH
1.17.3
2.7.2
+2 -1
View File
@@ -65,12 +65,13 @@ class InMemoryQCounter
def reload_stats!
require 'yaml'
require 'couchrest'
require 'pry'
#require 'pry'
couch_settings_path = 'config/couchdb.yml'
puts "Couch settings path: #{couch_settings_path}"
puts "Environment: #{environment.inspect}"
#couch_settings = YAML.load_file(couch_settings_path)[environment]
couch_settings = YAML.safe_load(ERB.new(File.read(couch_settings_path)).result, permitted_classes: [Symbol])[environment]
puts "Couch-Settings: \n#{couch_settings.to_yaml}"
database = couch_settings['database']
#database = database.sub 'localhost', 'host.docker.internal' unless environment == 'development'
#database = couch_settings['database']
+35 -5
View File
@@ -1,15 +1,44 @@
#!/usr/bin/env bash
# This script rebuilds the whole environment and starts a new container
#
# Set the prompt message
environments=("production" "development" "test" "Quit")
echo "Choose the environment (1-3): "
for i in "${!environments[@]}"; do
echo "$((i+1))) ${environments[$i]}"
done
read -p "Enter number: " choice
if [[ $choice -eq ${#environments[@]} ]]; then
exit 0
fi
# Validate and use the choice (subtract 1 for 0-based array index)
if [[ $choice -gt 0 && $choice -lt ${#environments[@]} ]]; then
environment=${environments[$((choice-1))]}
echo "You selected: $environment"
else
echo "Invalid selection."
exit 0
fi
# 1. ensure this script is run from the project's root, not the drb_counter directory
pwd_dirname=$(basename $(pwd));
script_dirname="drb_counter";
arch=$(uname)
environment="${1:-production}"
if [ "$pwd_dirname" == "$script_dirname" ]; then
echo "PWD DIRNAME: "$pwd_dirname;
echo "You must run this script from the project's root dir (../) for the Dockerfile to have access to the configs to COPY";
exit 1;
# Check for sanity, was debugging database mismatch
if [ "$#" -lt 1 ]; then
nodename=$(uname -n)
known_development_machines=("fedorasahi" "blackview")
for item in "${known_development_machines[@]}"; do
if [[ "$item" == "$nodename" ]]; then
echo "Stupid Error: You are on a known development device: $nodename. As a developer, always explicitly supply the environment as the first argument" >&2
echo "Usage: ./drb_counter/rebuild-docker.sh development"
exit 1
fi
done
fi
# 2. stop and remove all running/existing containers
@@ -31,6 +60,7 @@ if [ $arch == "Darwin" ]; then
else
# docker run --network=host --env DRB_ENV=production --env COUCHDB_ADMIN_PASSWORD=$COUCHDB_ADMIN_PASSWORD --add-host=host.docker.internal:host-gateway --restart unless-stopped --detach --name=mozo_drb_counter mozo_drb_counter
docker run --network=host --env DRB_ENV=$environment --env COUCHDB_ADMIN_PASSWORD=$COUCHDB_ADMIN_PASSWORD --restart unless-stopped --detach --name=mozo_drb_counter mozo_drb_counter
#docker run -p 9022:9022 --env DRB_ENV=$environment --env COUCHDB_ADMIN_PASSWORD=$COUCHDB_ADMIN_PASSWORD --restart unless-stopped --detach --name=mozo_drb_counter mozo_drb_counter
fi
# To just start the container created through al these steps without rebuilding them:
+2 -2
View File
@@ -19,12 +19,12 @@ module Mozo
autoload :DrbCounter
def self.broadcast_user(uid, event, data)
message = {channel: "/user/#{uid}", data: {event: event, data: data}}
message = {channel: "/user_#{uid}", data: {event: event, data: data}}
broadcaster.broadcast message
end
def self.broadcast_supplier(sid, event, data)
message = {channel: "/supplier/#{sid}", data: {event: event, data: data}}
message = {channel: "/supplier_#{sid}", data: {event: event, data: data}}
broadcaster.broadcast message
end
end
+1
View File
@@ -2,5 +2,6 @@ module Mozo
module Broadcaster
extend ActiveSupport::Autoload
autoload :Faye
autoload :ActionCable
end
end
+55
View File
@@ -0,0 +1,55 @@
# frozen_string_literal: true
module Mozo
module Broadcaster
# Drop-in replacement for Mozo::Broadcaster::Faye that uses
# Rails' built-in ActionCable instead of an external Faye process.
#
# Benefits over Faye:
# - Async by default (ActionCable.server.broadcast is non-blocking)
# - No extra gem / process / port to manage
# - Integrated with Rails authentication (cookies, sessions)
# - WebSocket native (no long-polling fallback needed with modern browsers)
#
# Channel naming: accepts both Faye format and underscore format:
# /user/123 or /user_123 → user_123
# /supplier/456 or /supplier_456 → supplier_456
#
# To use:
# Set Mozo.broadcaster = Mozo::Broadcaster::ActionCable.new
# in config/initializers/mozo_settings.rb
#
class ActionCable
CHANNEL_PREFIX_REMAP = {
%r{^/user[/_](.+)$} => 'user_\1',
%r{^/supplier[/_](.+)$} => 'supplier_\1'
}.freeze
def broadcast(message)
channel = message[:channel] || message['channel']
data = message[:data] || message['data']
remapped = remap_channel(channel)
unless remapped
Rails.logger.warn("[ACTION_CABLE] broadcast skipped: unknown channel #{channel}")
return
end
Rails.logger.debug("[ACTION_CABLE] broadcasting to #{remapped}: #{data.inspect}")
::ActionCable.server.broadcast(remapped, data)
rescue => e
Rails.logger.error("[ACTION_CABLE][ERROR] #{e.message}")
end
private
def remap_channel(channel)
CHANNEL_PREFIX_REMAP.each do |pattern, replacement|
return channel.sub(pattern, replacement) if channel.match?(pattern)
end
Rails.logger.warn("[ACTION_CABLE] Unknown channel format: #{channel}")
nil
end
end
end
end
+3
View File
@@ -1,5 +1,8 @@
module Mozo
module Counter
extend ActiveSupport::Autoload
autoload :Redis
mattr_accessor :connection
# mainly for testing purposes
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
module Mozo
module Counter
# Redis-backed counter adapter. Replaces the DrbCounter (in-memory,
# single-process, DRb-based) with a persistent, multi-process-safe,
# battle-tested key-value store.
#
# Benefits over DrbCounter:
# - Persistent (survives restarts, no CouchDB reload dance)
# - Atomic INCR/DECR (no race conditions between Puma workers)
# - No separate process to manage (Redis is already needed for ActionCable)
# - Production-ready, widely deployed
#
# Usage:
# Mozo::Counter.connection = Mozo::Counter::Redis.new
# # or with custom config:
# Mozo::Counter.connection = Mozo::Counter::Redis.new(url: "redis://localhost:6379/2")
#
class Redis
def initialize(url: nil)
require 'redis'
@redis = ::Redis.new(url: url || ENV.fetch('REDIS_URL', 'redis://localhost:6379/7'))
end
def get(key, options = {})
value = @redis.get(key)
quiet = options[:quiet]
unless quiet
Rails.logger.debug("[REDIS_COUNTER] GET #{key} = #{value.inspect}")
end
value&.to_i || 0
rescue => e
Rails.logger.error("[REDIS_COUNTER] GET #{key} failed: #{e.message}")
0
end
def set(key, value)
Rails.logger.debug("[REDIS_COUNTER] SET #{key} = #{value}")
@redis.set(key, value)
rescue => e
Rails.logger.error("[REDIS_COUNTER] SET #{key} failed: #{e.message}")
value
end
def incr(key, options = {})
initial = options[:initial] || 1
Rails.logger.debug("[REDIS_COUNTER] INCR #{key}")
if @redis.exists?(key)
@redis.incr(key)
else
@redis.set(key, initial)
initial
end
rescue => e
Rails.logger.error("[REDIS_COUNTER] INCR #{key} failed: #{e.message}")
initial
end
def decr(key, options = {})
initial = options[:initial] || 0
Rails.logger.debug("[REDIS_COUNTER] DECR #{key}")
if @redis.exists?(key)
@redis.decr(key)
else
@redis.set(key, initial)
initial
end
rescue => e
Rails.logger.error("[REDIS_COUNTER] DECR #{key} failed: #{e.message}")
initial
end
def flush
Rails.logger.debug("[REDIS_COUNTER] FLUSHDB")
@redis.flushdb
end
end
end
end
+5
View File
@@ -4,5 +4,10 @@ module Mozo
require 'drb'
DRbObject.new_with_uri('druby://localhost:9022')
end
# propagation method. Might save some searching. Hint: drb_counter/drb_counter.rb
def self.reload_stats!
object.reload_stats!
end
end
end

Some files were not shown because too many files have changed in this diff Show More