mission_control-web 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +122 -15
  4. data/Rakefile +21 -3
  5. data/app/assets/config/mission_control_web_manifest.js +2 -0
  6. data/app/assets/stylesheets/mission_control/web/application.css +15 -0
  7. data/app/assets/stylesheets/mission_control/web/vendor/bulma.min.css +1 -0
  8. data/app/controllers/concerns/mission_control/web/application_scoped.rb +17 -0
  9. data/app/controllers/mission_control/web/application_controller.rb +4 -0
  10. data/app/controllers/mission_control/web/errors_controller.rb +6 -0
  11. data/app/controllers/mission_control/web/routes_controller.rb +52 -0
  12. data/app/helpers/mission_control/web/applications_helper.rb +9 -0
  13. data/app/helpers/mission_control/web/routes_helper.rb +9 -0
  14. data/app/javascript/mission_control/web/application.js +1 -0
  15. data/app/models/concerns/mission_control/web/route/applications.rb +17 -0
  16. data/app/models/mission_control/web/application/routes.rb +24 -0
  17. data/app/models/mission_control/web/application.rb +34 -0
  18. data/app/models/mission_control/web/application_record.rb +3 -0
  19. data/app/models/mission_control/web/current.rb +8 -0
  20. data/app/models/mission_control/web/route.rb +14 -0
  21. data/app/views/layouts/mission_control/web/application.html.erb +23 -0
  22. data/app/views/mission_control/web/application/_current_application_selector.html.erb +15 -0
  23. data/app/views/mission_control/web/application/_flash.html.erb +3 -0
  24. data/app/views/mission_control/web/application/_form_errors.html.erb +11 -0
  25. data/app/views/mission_control/web/application/_navbar.html.erb +14 -0
  26. data/app/views/mission_control/web/errors/disallowed.html.erb +8 -0
  27. data/app/views/mission_control/web/routes/_form.html.erb +26 -0
  28. data/app/views/mission_control/web/routes/_route.html.erb +13 -0
  29. data/app/views/mission_control/web/routes/edit.html.erb +8 -0
  30. data/app/views/mission_control/web/routes/index.html.erb +27 -0
  31. data/app/views/mission_control/web/routes/new.html.erb +8 -0
  32. data/app/views/mission_control/web/routes/show.html.erb +42 -0
  33. data/config/importmap.rb +2 -0
  34. data/config/routes.rb +7 -0
  35. data/db/migrate/20221025093008_create_mission_control_web_routes.rb +14 -0
  36. data/lib/generators/mission_control/web/install/admin_generator.rb +38 -0
  37. data/lib/generators/mission_control/web/install/middleware_generator.rb +26 -0
  38. data/lib/generators/mission_control/web/install_generator.rb +6 -0
  39. data/lib/mission_control/web/action_dispatch_request.rb +7 -0
  40. data/lib/mission_control/web/bulma_form_builder.rb +50 -0
  41. data/lib/mission_control/web/configuration.rb +21 -0
  42. data/lib/mission_control/web/engine.rb +45 -0
  43. data/lib/mission_control/web/errors/resource_not_found.rb +2 -0
  44. data/lib/mission_control/web/request_filter.rb +17 -0
  45. data/lib/mission_control/web/request_filter_middleware.rb +26 -0
  46. data/lib/mission_control/web/routes_cache.rb +52 -0
  47. data/lib/mission_control/web/version.rb +1 -3
  48. data/lib/mission_control/web.rb +18 -4
  49. data/lib/tasks/mission_control/web_tasks.rake +4 -0
  50. metadata +147 -8
  51. data/LICENSE.txt +0 -21
  52. data/mission_control-web.gemspec +0 -35
  53. data/sig/mission_control/web.rbs +0 -6
@@ -0,0 +1,24 @@
1
+ module MissionControl::Web::Application::Routes
2
+ extend ActiveSupport::Concern
3
+
4
+ def routes
5
+ MissionControl::Web::Route.where(application_id: id)
6
+ end
7
+
8
+ def route_was_updated(route)
9
+ routes_cache.put(route)
10
+ end
11
+
12
+ def route_was_deleted(route)
13
+ routes_cache.remove(route)
14
+ end
15
+
16
+ def route_disabled?(path)
17
+ routes_cache.disabled?(path)
18
+ end
19
+
20
+ private
21
+ def routes_cache
22
+ @routes_cache ||= MissionControl::Web::RoutesCache.new(self)
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ class MissionControl::Web::Application
2
+ include ActiveModel::Model
3
+ include Routes
4
+
5
+ attr_accessor :name, :redis
6
+
7
+ class << self
8
+ def all
9
+ MissionControl::Web.configuration.administered_applications.map { |app| new(**app) }
10
+ end
11
+
12
+ def find(id)
13
+ all.find { |application| application.id == id }
14
+ end
15
+
16
+ def find!(id)
17
+ find(id) or raise MissionControl::Web::Errors::ResourceNotFound
18
+ end
19
+
20
+ def find_or_initialize_by_name(name)
21
+ find(name.parameterize) || new(name: name)
22
+ end
23
+
24
+ def default
25
+ all.first or raise MissionControl::Web::Errors::ResourceNotFound
26
+ end
27
+ end
28
+
29
+ def id
30
+ name.parameterize
31
+ end
32
+
33
+ alias to_param id
34
+ end
@@ -0,0 +1,3 @@
1
+ class MissionControl::Web::ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,8 @@
1
+ class MissionControl::Web::Current < ActiveSupport::CurrentAttributes
2
+ attribute :application
3
+
4
+ def application=(application)
5
+ super
6
+ MissionControl::Web.current_redis = application.redis
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ class MissionControl::Web::Route < ApplicationRecord
2
+ include Applications
3
+
4
+ validates :name, :pattern, presence: true
5
+ validates :pattern, uniqueness: { scope: :application_id }
6
+
7
+ def application
8
+ @application ||= MissionControl::Web::Application.find!(application_id)
9
+ end
10
+
11
+ def disabled?
12
+ !enabled?
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Mission Control - Web</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "mission_control/web/application", "data-turbo-track": "reload" %>
9
+ <%= javascript_importmap_tags %>
10
+ </head>
11
+
12
+ <body>
13
+ <div class="container">
14
+ <section class="section">
15
+ <%= render "navbar" %>
16
+ <%= render "flash" %>
17
+
18
+ <%= yield %>
19
+
20
+ </section>
21
+ </div>
22
+ </body>
23
+ </html>
@@ -0,0 +1,15 @@
1
+ <% if selectable_applications.any? %>
2
+ <div class="application-selector navbar-item has-dropdown is-hoverable">
3
+ <a class="navbar-link">
4
+ <%= MissionControl::Web::Current.application.name %>
5
+ </a>
6
+
7
+ <div class="navbar-dropdown">
8
+ <% selectable_applications.each do |application| %>
9
+ <%= link_to application_routes_path(application), class: "navbar-item" do %>
10
+ <span><%= application.name %></span>
11
+ <% end %>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% if notice %>
2
+ <div class="notification"><%= notice %></div>
3
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <% if model.errors.any? %>
2
+ <div class="notification content">
3
+ <h3 class="subtitle is-6"><%= pluralize(model.errors.count, "error") %> prohibited this <%= model.class.name.demodulize %> from being saved:</h3>
4
+
5
+ <ul>
6
+ <% model.errors.each do |error| %>
7
+ <li><%= error.full_message %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <nav class="navbar" role="navigation" aria-label="main navigation">
2
+ <div class="navbar-brand">
3
+ <h1 class="title">Mission Control - Web</h1>
4
+ </div>
5
+
6
+ <div class="navbar-menu is-active">
7
+ <div class="navbar-start">
8
+ </div>
9
+
10
+ <div class="navbar-end">
11
+ <%= render "current_application_selector" %>
12
+ </div>
13
+ </div>
14
+ </nav>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>Service temporarily unavailable</title>
4
+ </head>
5
+ <body>
6
+ <p>Service temporarily unavailable. Try again in a few minutes.</p>
7
+ </body>
8
+ </html>
@@ -0,0 +1,26 @@
1
+ <%= form_with(model: [ route.application, route] ) do |form| %>
2
+ <%= render partial: "form_errors", locals: { model: route } %>
3
+
4
+ <div class="field">
5
+ <%= form.label :name %>
6
+ <%= form.text_field :name %>
7
+ </div>
8
+
9
+ <div class="field">
10
+ <%= form.label :pattern %>
11
+ <%= form.text_field :pattern %>
12
+ </div>
13
+
14
+ <div class="field">
15
+ <%= form.label_check_box :enabled, "Traffic allowed" %>
16
+ <%= form.check_box :enabled %>
17
+ </div>
18
+
19
+ <div class="field is-grouped">
20
+ <%= form.submit_primary %>
21
+
22
+ <%= form.div_control do %>
23
+ <%= link_to "Cancel", application_routes_path(route.application), class: "button is-link is-light" %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <tr>
2
+ <td>
3
+ <%= link_to route.name, [ application, route ] %>
4
+ </td>
5
+
6
+ <td>
7
+ <%= route.pattern %>
8
+ </td>
9
+
10
+ <td>
11
+ <%= route_status_tag route.enabled %>
12
+ </td>
13
+ </tr>
@@ -0,0 +1,8 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><a>Editing <%= @route.name %></a></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <%= render "form", route: @route %>
@@ -0,0 +1,27 @@
1
+ <div class="level">
2
+ <div class="level-left">
3
+ <h2 class="subtitle level-item"><%= @application.name %> - Routes</h2>
4
+ </div>
5
+ <div class="level-right">
6
+ <%= link_to "New Route", new_application_route_path(@application), class: "button is-primary level-item" %>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="content">
11
+ <% if @routes.any? %>
12
+ <table class="table">
13
+ <thead>
14
+ <tr>
15
+ <th>Name</th>
16
+ <th>Pattern</th>
17
+ <th>Traffic Status</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <%= render @routes, application: @application %>
22
+ </tbody>
23
+ </table>
24
+ <% else %>
25
+ <p>No Routes have yet been configured.</p>
26
+ <% end %>
27
+ </div>
@@ -0,0 +1,8 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><a>New Route</a></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <%= render "form", route: @route %>
@@ -0,0 +1,42 @@
1
+ <nav class="breadcrumb" aria-label="breadcrumbs">
2
+ <ul>
3
+ <li><%= link_to @application.name, application_routes_path(@application) %></li>
4
+ <li class="is-active"><%= link_to @route.name, [ @application, @route ] %></li>
5
+ </ul>
6
+ </nav>
7
+
8
+ <div class="my-4">
9
+ <div class="columns">
10
+ <div class="column is-one-fifth">
11
+ <p class="has-text-weight-semibold">Name</p>
12
+ </div>
13
+ <div class="column">
14
+ <p><%= @route.name %></p>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="columns">
19
+ <div class="column is-one-fifth">
20
+ <p class="has-text-weight-semibold">Pattern</p>
21
+ </div>
22
+ <div class="column">
23
+ <p><%= @route.pattern %></p>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="columns">
28
+ <div class="column is-one-fifth">
29
+ <p class="has-text-weight-semibold">Traffic Status</p>
30
+ </div>
31
+ <div class="column">
32
+ <p><%= route_status_tag @route.enabled %></p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="level">
38
+ <div class="level-left">
39
+ <%= link_to "Edit", edit_application_route_path(@application, @route), class: "button level-item" %>
40
+ <%= button_to "Delete", [ @application, @route ], method: :delete, class: "button is-danger level-item" %>
41
+ </div>
42
+ </div>
@@ -0,0 +1,2 @@
1
+ pin "application", to: "mission_control/web/application.js", preload: true
2
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ MissionControl::Web::Engine.routes.draw do
2
+ resources :applications, only: [] do
3
+ resources :routes
4
+ end
5
+
6
+ root to: "routes#index"
7
+ end
@@ -0,0 +1,14 @@
1
+ class CreateMissionControlWebRoutes < ActiveRecord::Migration[7.0]
2
+ def up
3
+ create_table :mission_control_web_routes do |t|
4
+ t.string :name, null: false
5
+ t.string :pattern, null: false
6
+ t.boolean :enabled, default: true
7
+ t.string :application_id, null: false, index: true
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :mission_control_web_routes, [ :application_id, :pattern ], unique: true
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module MissionControl::Web::Install
2
+ class AdminGenerator < Rails::Generators::Base
3
+ INITIALIZER_FILE_PATH = "config/initializers/mission_control_web.rb"
4
+
5
+ def create_initializer_file
6
+ create_file INITIALIZER_FILE_PATH, <<~RUBY, skip: true
7
+ Rails.application.configure do
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ def insert_admin_configuration
13
+ initializer = <<~RUBY
14
+ config.mission_control.web.middleware_enabled = false
15
+
16
+ # Admin
17
+ config.mission_control.web.administered_applications = [
18
+ { name: "My App", redis: Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")) }
19
+ ]
20
+ RUBY
21
+
22
+ insert_into_file INITIALIZER_FILE_PATH, indent(initializer), after: "Rails.application.configure do\n"
23
+ end
24
+
25
+ def add_engine_routes
26
+ route "mount MissionControl::Web::Engine => '/mission_control-web'"
27
+ end
28
+
29
+ def copy_migrations
30
+ rake "mission_control_web:install:migrations"
31
+ end
32
+
33
+ def run_migrations
34
+ say "Running migrations..."
35
+ rails_command "db:migrate"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ module MissionControl::Web::Install
2
+ class MissionControl::Web::Install::MiddlewareGenerator < Rails::Generators::Base
3
+ INITIALIZER_FILE_PATH = "config/initializers/mission_control_web.rb"
4
+
5
+ def create_initializer_file
6
+ create_file INITIALIZER_FILE_PATH, <<~RUBY, skip: true
7
+ Rails.application.configure do
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ def insert_middleware_configuration
13
+ initializer = <<~RUBY
14
+ # Middleware
15
+ config.mission_control.web.host_application_name = "My App"
16
+ config.mission_control.web.redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
17
+ RUBY
18
+
19
+ insert_into_file INITIALIZER_FILE_PATH, indent(initializer), after: "Rails.application.configure do\n"
20
+ end
21
+
22
+ def toggle_middleware_enabled
23
+ gsub_file INITIALIZER_FILE_PATH, "middleware_enabled = false", "middleware_enabled = true"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ class MissionControl::Web::InstallGenerator < Rails::Generators::Base
2
+ def install_components
3
+ rails_command "generate mission_control:web:install:admin"
4
+ rails_command "generate mission_control:web:install:middleware"
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module MissionControl::Web
2
+ module ActionDispatchRequest
3
+ def mission_control
4
+ MissionControl::Web::RequestFilter.new(self)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ # Inspired by https://medium.com/@dyanagi/a-bulma-form-builder-for-ruby-on-rails-applications-aef780808bab
2
+
3
+ class MissionControl::Web::BulmaFormBuilder < ActionView::Helpers::FormBuilder
4
+ def label_default(method, text = nil, options = {}, &block)
5
+ label(method, text, merge_class(options, "label"), &block)
6
+ end
7
+
8
+ def label_check_box(method, text = nil, options = {}, &block)
9
+ label(method, text, merge_class(options, "checkbox"), &block)
10
+ end
11
+
12
+ def text_field(method, options = {})
13
+ super(method, merge_class(options, "input"))
14
+ end
15
+
16
+ def submit(value = nil, options = {})
17
+ div_control do
18
+ super(value, merge_class(options, "button"))
19
+ end
20
+ end
21
+
22
+ def submit_primary(value = nil, options = {})
23
+ submit(value, merge_class(options, "is-primary"))
24
+ end
25
+
26
+ def div_control
27
+ @template.content_tag(:div, class: "control") do
28
+ yield
29
+ end
30
+ end
31
+
32
+ def select_with_label(method, choices = nil, options = {}, html_options = {}, &block)
33
+ label_default(method) + select(method, choices, options, html_options, &block)
34
+ end
35
+
36
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
37
+ label(method, class: "select") do
38
+ super
39
+ end
40
+ end
41
+
42
+ private
43
+ def merge_class_attribute_value(options, value)
44
+ new_options = options.clone
45
+ new_options[:class] = [ value, new_options[:class] ].join(" ")
46
+ new_options
47
+ end
48
+
49
+ alias_method :merge_class, :merge_class_attribute_value
50
+ end
@@ -0,0 +1,21 @@
1
+ class MissionControl::Web::Configuration
2
+ include ActiveModel::Attributes, ActiveModel::Dirty
3
+
4
+ attribute :host_application_name, :string
5
+ attribute :middleware_enabled, :boolean, default: true
6
+ attribute :routes_cache_ttl, :integer, default: 10.seconds
7
+ attribute :base_controller_class, :string, default: "::ApplicationController"
8
+
9
+ attr_writer :redis, :administered_applications
10
+ attr_accessor :errors_controller
11
+
12
+ alias :middleware_enabled? :middleware_enabled
13
+
14
+ def redis
15
+ @redis || raise("Redis client not configured. Configure with MissionControl::Web.configuration.redis = Redis.new")
16
+ end
17
+
18
+ def administered_applications
19
+ @administered_applications || []
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ require "importmap-rails"
2
+ require "turbo-rails"
3
+
4
+ module MissionControl
5
+ module Web
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace MissionControl::Web
8
+
9
+ config.mission_control = ActiveSupport::OrderedOptions.new unless config.try(:mission_control)
10
+ config.mission_control.web = ActiveSupport::OrderedOptions.new
11
+
12
+ initializer "mission_control-web.config" do
13
+ config.mission_control.web.each do |key, value|
14
+ MissionControl::Web.configuration.public_send("#{key}=", value)
15
+ end
16
+ end
17
+
18
+ initializer "mission_control-web.request" do
19
+ ActiveSupport.on_load :action_dispatch_request do
20
+ include MissionControl::Web::ActionDispatchRequest
21
+ end
22
+ end
23
+
24
+ initializer "mission_control-web.add_middleware", after: "mission_control-web.config" do |app|
25
+ if MissionControl::Web.configuration.middleware_enabled?
26
+ app.middleware.use MissionControl::Web::RequestFilterMiddleware
27
+ end
28
+ end
29
+
30
+ initializer "mission_control-web.assets" do |app|
31
+ app.config.assets.paths << root.join("app/javascript")
32
+ app.config.assets.precompile += %w[ mission_control_web_manifest ]
33
+ end
34
+
35
+ initializer "mission_control-web.importmap", before: "importmap" do |app|
36
+ app.config.importmap.paths << root.join("config/importmap.rb")
37
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
38
+ end
39
+
40
+ config.after_initialize do
41
+ MissionControl::Web.configuration.host_application_name ||= ::Rails.application.class.module_parent.to_s
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,2 @@
1
+ class MissionControl::Web::Errors::ResourceNotFound < StandardError
2
+ end
@@ -0,0 +1,17 @@
1
+ module MissionControl::Web
2
+ class RequestFilter
3
+ attr_reader :request
4
+
5
+ def initialize(request)
6
+ @request = request
7
+ end
8
+
9
+ def action
10
+ request.fetch_header("mission_control.action") do
11
+ if MissionControl::Web.host_application.route_disabled?(request.path)
12
+ :disallowed
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module MissionControl::Web
2
+ class RequestFilterMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ filter(env) || @app.call(env)
9
+ end
10
+
11
+ def filter(env)
12
+ return unless MissionControl::Web.configuration.middleware_enabled?
13
+
14
+ request = ActionDispatch::Request.new(env)
15
+
16
+ if action = request.mission_control.action
17
+ errors_controller.action(action).call(request.env)
18
+ end
19
+ end
20
+
21
+ private
22
+ def errors_controller
23
+ MissionControl::Web.configuration.errors_controller || MissionControl::Web::ErrorsController
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ class MissionControl::Web::RoutesCache
2
+ delegate :redis, to: MissionControl::Web
3
+
4
+ def initialize(application)
5
+ @application = application
6
+ end
7
+
8
+ def put(route)
9
+ remove(route)
10
+
11
+ if route.disabled?
12
+ add(route)
13
+ end
14
+ end
15
+
16
+ def remove(route)
17
+ redis.srem redis_key, [ route.pattern_previously_was.to_s, route.pattern.to_s ]
18
+ end
19
+
20
+ def disabled?(path)
21
+ all_disabled_patterns.any? { |pattern| Regexp.new(pattern) =~ path }
22
+ end
23
+
24
+ private
25
+ attr_reader :application
26
+
27
+ def add(route)
28
+ redis.sadd redis_key, route.pattern.to_s
29
+ end
30
+
31
+ def redis_key
32
+ :"mission_control_web_#{application.id}_disabled_patterns"
33
+ end
34
+
35
+ def all_disabled_patterns
36
+ memoize(ttl: MissionControl::Web.configuration.routes_cache_ttl) do
37
+ # Using Redis client rather than Kredis as request interception with a middleware is performance-critical.
38
+ redis.smembers redis_key
39
+ end
40
+ rescue Redis::BaseConnectionError
41
+ []
42
+ end
43
+
44
+ def memoize(ttl:)
45
+ if !@patterns.nil? && @patterns_expires_at > Time.now
46
+ @patterns
47
+ else
48
+ @patterns_expires_at = Time.now + ttl
49
+ @patterns = yield
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  module MissionControl
4
2
  module Web
5
- VERSION = "0.0.1"
3
+ VERSION = "0.2.0"
6
4
  end
7
5
  end