s3arch 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e0ba51260081a3de075c6e14332909892693b64dce6eb1a3e52f7b1f145b537
4
- data.tar.gz: 7553e90741ef8a1cc6c72b55bb9239c0e238da2a09916d34e8edf18780942a24
3
+ metadata.gz: 154d95c7316c2400be5958f3722bf84175559c0e0a6febf739e661852de955b8
4
+ data.tar.gz: d869f08480bd60443e7de00cb82042143a571134c7b2f1c14e4d2e56586be20c
5
5
  SHA512:
6
- metadata.gz: 26e0cd3ff75ea9aff46dfd28a5978db557b26cef4780d966345b14fdb0b023bc9a6d22809c30c1422ac967aea3cbcc7bf18a75e94ea07d56af13d0f96d2d12e2
7
- data.tar.gz: bf5c5b19de20e235f95427218b8beb0ee5ab03b4270bf31cbe38ca2f6f420c5665eb5b2a44e65461997a12412d4256099edaf3bc5be4ddb9f53f73105aaa3e79
6
+ metadata.gz: 0ee5a7e1426fad5be655a050b466ddb18dd34f3c61694d7fba9cbd7262ee87da867b1669493773a1c4ff803605bf70da74f8cef8ff6cbafff86c54fe1822d175
7
+ data.tar.gz: 0e35593c714d9bb102076026bb4d64a1bc0bf56bc11489f8864b1fd5b3cf941a5f0c4aa34e88dc1cb4df412a85871ea0acb48002c15243d6536ec3f477f52e4b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,20 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
4
3
  require 'aws-sdk-dynamodb'
5
4
 
6
5
  class S3archController < BeltController::Base
7
- VIEWS_PATH = File.expand_path('../../lib/s3arch/dashboard/views', __dir__)
8
-
9
6
  def index
10
- @owners = fetch_owners
11
- html = render_erb('index')
12
- html_response(html)
7
+ owners = fetch_owners
8
+ success_response(owners: owners)
13
9
  rescue StandardError => e
14
- @error = e.message
15
- @owners = []
16
- html = render_erb('index')
17
- html_response(html, 500)
10
+ error_response("Failed to load owners: #{e.message}", 500)
18
11
  end
19
12
 
20
13
  def rebuild
@@ -58,9 +51,4 @@ class S3archController < BeltController::Base
58
51
  end
59
52
  owners.sort_by { |o| o[:updated_at].to_s }.reverse
60
53
  end
61
-
62
- def render_erb(template)
63
- path = File.join(VIEWS_PATH, "#{template}.html.erb")
64
- ERB.new(File.read(path), trim_mode: '-').result(binding)
65
- end
66
54
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module S3arch
4
+ class Indexer
5
+ # Parses DynamoDB Stream events from SQS records into change operations.
6
+ module StreamParser
7
+ private
8
+
9
+ def group_changes(sqs_records)
10
+ grouped = Hash.new { |h, k| h[k] = [] }
11
+
12
+ sqs_records.each do |sqs_record|
13
+ stream_record = JSON.parse(sqs_record['body'])
14
+ event_name = stream_record['eventName']
15
+ new_image = stream_record.dig('dynamodb', 'NewImage')
16
+ old_image = stream_record.dig('dynamodb', 'OldImage')
17
+
18
+ owner_id = extract_owner(new_image || old_image)
19
+ next unless owner_id
20
+
21
+ change = build_change(event_name, new_image, old_image)
22
+ grouped[owner_id] << change if change
23
+ end
24
+
25
+ grouped
26
+ end
27
+
28
+ def build_change(event_name, new_image, old_image)
29
+ case event_name
30
+ when 'INSERT'
31
+ tokens = extract_tokens(new_image)
32
+ record_id = extract_record_id(new_image)
33
+ return nil unless tokens && record_id && passes_filter?(new_image)
34
+
35
+ { action: :insert, record_id: record_id, tokens: tokens, meta: extract_meta(new_image) }
36
+ when 'REMOVE'
37
+ tokens = extract_tokens(old_image)
38
+ record_id = extract_record_id(old_image)
39
+ return nil unless tokens && record_id
40
+
41
+ { action: :delete, record_id: record_id, tokens: tokens }
42
+ when 'MODIFY'
43
+ build_modify_change(new_image, old_image)
44
+ end
45
+ end
46
+
47
+ def build_modify_change(new_image, old_image)
48
+ old_tokens = extract_tokens(old_image)
49
+ new_tokens = extract_tokens(new_image)
50
+ record_id = extract_record_id(new_image)
51
+ return nil unless record_id
52
+
53
+ unless passes_filter?(new_image)
54
+ return old_tokens ? { action: :delete, record_id: record_id, tokens: old_tokens } : nil
55
+ end
56
+
57
+ unless old_tokens
58
+ return new_tokens ? { action: :insert, record_id: record_id, tokens: new_tokens,
59
+ meta: extract_meta(new_image) } : nil
60
+ end
61
+
62
+ return nil unless new_tokens
63
+
64
+ { action: :update, record_id: record_id, old_tokens: old_tokens, new_tokens: new_tokens,
65
+ meta: extract_meta(new_image) }
66
+ end
67
+
68
+ def extract_owner(image)
69
+ return nil unless image
70
+
71
+ val = image[@config.owner_key]
72
+ val.is_a?(Hash) ? val['S'] : val
73
+ end
74
+
75
+ def extract_record_id(image)
76
+ return nil unless image
77
+
78
+ val = image['id']
79
+ val.is_a?(Hash) ? val['S'] : val
80
+ end
81
+
82
+ def extract_tokens(image)
83
+ return nil unless image
84
+
85
+ val = image[@config.token_field]
86
+ return nil unless val
87
+
88
+ if val.is_a?(Hash) && val.key?('M')
89
+ val['M'].transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
90
+ elsif val.is_a?(Hash) && !val.key?('S')
91
+ val.transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
92
+ end
93
+ end
94
+
95
+ def extract_meta(image)
96
+ @config.metadata_fields.each_with_object({}) do |field, meta|
97
+ val = image[field]
98
+ meta[field] = val.is_a?(Hash) ? (val['S'] || val['N'] || '') : val.to_s
99
+ end
100
+ end
101
+
102
+ def passes_filter?(image)
103
+ plain = image.transform_values { |v| v.is_a?(Hash) ? (v['S'] || v['N'] || v['BOOL']&.to_s || '') : v }
104
+ @config.record_filter.call(plain)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -4,11 +4,15 @@ require 'aws-sdk-dynamodb'
4
4
  require 'aws-sdk-s3'
5
5
  require 'sqlite3'
6
6
  require 'json'
7
+ require_relative 'indexer/stream_parser'
8
+
7
9
  module S3arch
8
10
  # Builds SQLite FTS5 databases per owner from pre-computed tokens stored in DynamoDB.
9
11
  # The indexer never sees raw content — only tokens. Supports incremental updates via
10
12
  # DynamoDB Stream events (INSERT/MODIFY/REMOVE).
11
13
  class Indexer
14
+ include StreamParser
15
+
12
16
  def initialize(config: S3arch.configuration)
13
17
  config.validate!
14
18
  @config = config
@@ -83,104 +87,6 @@ module S3arch
83
87
 
84
88
  private
85
89
 
86
- def group_changes(sqs_records)
87
- grouped = Hash.new { |h, k| h[k] = [] }
88
-
89
- sqs_records.each do |sqs_record|
90
- stream_record = JSON.parse(sqs_record['body'])
91
- event_name = stream_record['eventName']
92
- new_image = stream_record.dig('dynamodb', 'NewImage')
93
- old_image = stream_record.dig('dynamodb', 'OldImage')
94
-
95
- owner_id = extract_owner(new_image || old_image)
96
- next unless owner_id
97
-
98
- change = build_change(event_name, new_image, old_image)
99
- grouped[owner_id] << change if change
100
- end
101
-
102
- grouped
103
- end
104
-
105
- def build_change(event_name, new_image, old_image)
106
- case event_name
107
- when 'INSERT'
108
- tokens = extract_tokens(new_image)
109
- record_id = extract_record_id(new_image)
110
- return nil unless tokens && record_id && passes_filter?(new_image)
111
-
112
- { action: :insert, record_id: record_id, tokens: tokens, meta: extract_meta(new_image) }
113
- when 'REMOVE'
114
- tokens = extract_tokens(old_image)
115
- record_id = extract_record_id(old_image)
116
- return nil unless tokens && record_id
117
-
118
- { action: :delete, record_id: record_id, tokens: tokens }
119
- when 'MODIFY'
120
- old_tokens = extract_tokens(old_image)
121
- new_tokens = extract_tokens(new_image)
122
- record_id = extract_record_id(new_image)
123
- return nil unless record_id
124
-
125
- # If item no longer passes filter, treat as delete
126
- unless passes_filter?(new_image)
127
- return old_tokens ? { action: :delete, record_id: record_id, tokens: old_tokens } : nil
128
- end
129
-
130
- # If item previously didn't pass filter (no old tokens), treat as insert
131
- unless old_tokens
132
- return new_tokens ? { action: :insert, record_id: record_id, tokens: new_tokens,
133
- meta: extract_meta(new_image) } : nil
134
- end
135
-
136
- return nil unless new_tokens
137
-
138
- { action: :update, record_id: record_id, old_tokens: old_tokens, new_tokens: new_tokens,
139
- meta: extract_meta(new_image) }
140
- end
141
- end
142
-
143
- def extract_owner(image)
144
- return nil unless image
145
-
146
- val = image[@config.owner_key]
147
- val.is_a?(Hash) ? val['S'] : val
148
- end
149
-
150
- def extract_record_id(image)
151
- return nil unless image
152
-
153
- val = image['id']
154
- val.is_a?(Hash) ? val['S'] : val
155
- end
156
-
157
- def extract_tokens(image)
158
- return nil unless image
159
-
160
- val = image[@config.token_field]
161
- return nil unless val
162
-
163
- # Token field is a DynamoDB Map: { "M": { "name": { "S": "..." }, "description": { "S": "..." } } }
164
- if val.is_a?(Hash) && val.key?('M')
165
- val['M'].transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
166
- elsif val.is_a?(Hash) && !val.key?('S')
167
- val.transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
168
- end
169
- end
170
-
171
- def extract_meta(image)
172
- @config.metadata_fields.each_with_object({}) do |field, meta|
173
- val = image[field]
174
- meta[field] = val.is_a?(Hash) ? (val['S'] || val['N'] || '') : val.to_s
175
- end
176
- end
177
-
178
- def passes_filter?(image)
179
- # Convert DynamoDB image to plain hash for filter
180
- plain = image.transform_values { |v| v.is_a?(Hash) ? (v['S'] || v['N'] || v['BOOL']&.to_s || '') : v }
181
- @config.record_filter.call(plain)
182
- end
183
-
184
90
  def apply_change(db, change)
185
91
  case change[:action]
186
92
  when :insert
@@ -252,7 +158,7 @@ module S3arch
252
158
  result.items.each do |item|
253
159
  next unless @config.record_filter.call(item)
254
160
 
255
- tokens = item[@config.token_field]
161
+ tokens = extract_tokens_from_item(item)
256
162
  next unless tokens.is_a?(Hash) && tokens.any?
257
163
 
258
164
  records << { 'id' => item['id'], 'tokens' => tokens, 'meta' => extract_meta_from_item(item) }
@@ -265,9 +171,21 @@ module S3arch
265
171
  records
266
172
  end
267
173
 
174
+ def extract_tokens_from_item(item)
175
+ # Try the dedicated token field first
176
+ tokens = item[@config.token_field]
177
+ return tokens if tokens.is_a?(Hash) && tokens.any?
178
+
179
+ # Fall back: synthesize tokens from searchable_fields directly
180
+ @config.searchable_fields.each_with_object({}) do |field, map|
181
+ val = item[field]
182
+ map[field] = val.to_s if val
183
+ end
184
+ end
185
+
268
186
  def build_query_params(owner_id)
269
187
  fields = (['id', @config.token_field, @config.owner_key] +
270
- @config.metadata_fields + @config.filter_fields).uniq
188
+ @config.searchable_fields + @config.metadata_fields + @config.filter_fields).compact.uniq
271
189
  expression_names = {}
272
190
  projected = fields.map { |f| reserved_word?(f) ? "##{f}".tap { |p| expression_names[p] = f } : f }
273
191
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module S3arch
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/s3arch/web.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Lightweight S3arch load for web/API contexts (no SQLite dependency).
4
- # Provides configuration, holster registration, and routes only.
4
+ # Provides configuration and routes only.
5
5
  require_relative 'version'
6
6
  require_relative 'configuration'
7
7
  require_relative 'routes'
8
- require_relative 'holster'
9
8
 
10
9
  module S3arch
11
10
  class Error < StandardError; end
data/lib/s3arch.rb CHANGED
@@ -7,10 +7,6 @@ require_relative 's3arch/indexer'
7
7
  require_relative 's3arch/searcher'
8
8
  require_relative 's3arch/handler'
9
9
  require_relative 's3arch/routes'
10
- require_relative 's3arch/dashboard'
11
-
12
- # Register as a Belt holster when Belt is loaded
13
- require_relative 's3arch/holster' if defined?(Belt::Holster)
14
10
 
15
11
  module S3arch
16
12
  class Error < StandardError; end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: s3arch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Dalton
@@ -109,12 +109,9 @@ files:
109
109
  - lambda/controllers/s3arch_controller.rb
110
110
  - lib/s3arch.rb
111
111
  - lib/s3arch/configuration.rb
112
- - lib/s3arch/dashboard.rb
113
- - lib/s3arch/dashboard/application.rb
114
- - lib/s3arch/dashboard/views/index.html.erb
115
112
  - lib/s3arch/handler.rb
116
- - lib/s3arch/holster.rb
117
113
  - lib/s3arch/indexer.rb
114
+ - lib/s3arch/indexer/stream_parser.rb
118
115
  - lib/s3arch/routes.rb
119
116
  - lib/s3arch/searcher.rb
120
117
  - lib/s3arch/tokenizer.rb
metadata.gz.sig CHANGED
Binary file
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'erb'
4
- require 'json'
5
- require 'aws-sdk-dynamodb'
6
-
7
- module S3arch
8
- module Dashboard
9
- class Application
10
- VIEWS_PATH = File.expand_path('views', __dir__)
11
-
12
- # Dispatcher mount DSL contract — keep in sync with #call dispatch below.
13
- def routes
14
- [
15
- { method: :get, path: '/' },
16
- { method: :post, path: '/rebuild' }
17
- ]
18
- end
19
-
20
- def call(env)
21
- req = Rack::Request.new(env)
22
- path = req.path_info.sub(%r{^/}, '')
23
-
24
- case [req.request_method, path]
25
- when ['GET', ''], %w[GET index]
26
- index(req)
27
- when %w[POST rebuild]
28
- rebuild(req)
29
- else
30
- [404, { 'content-type' => 'text/plain' }, ['Not Found']]
31
- end
32
- end
33
-
34
- private
35
-
36
- def index(req)
37
- @env = req.env
38
- @owners = fetch_owners
39
- html = render('index')
40
- [200, { 'content-type' => 'text/html' }, [html]]
41
- end
42
-
43
- def rebuild(req)
44
- owner_id = req.params['owner_id']&.strip
45
- if owner_id && !owner_id.empty?
46
- indexer = S3arch::Indexer.new
47
- indexer.rebuild(owner_id)
48
- end
49
- [303, { 'location' => "#{req.script_name}/" }, []]
50
- end
51
-
52
- def fetch_owners
53
- config = S3arch.configuration
54
- dynamodb = Aws::DynamoDB::Client.new
55
- items = []
56
- params = { table_name: config.version_table }
57
-
58
- loop do
59
- result = dynamodb.scan(params)
60
- items.concat(result.items)
61
- break unless result.last_evaluated_key
62
-
63
- params[:exclusive_start_key] = result.last_evaluated_key
64
- end
65
-
66
- owners = items.map do |item|
67
- {
68
- owner_id: item[config.owner_key] || item.values.first,
69
- version: item['version'],
70
- record_count: item['record_count'],
71
- updated_at: item['updated_at']
72
- }
73
- end
74
- owners.sort_by { |o| o[:updated_at].to_s }.reverse
75
- rescue StandardError => e
76
- @error = e.message
77
- []
78
- end
79
-
80
- def render(template)
81
- path = File.join(VIEWS_PATH, "#{template}.html.erb")
82
- ERB.new(File.read(path), trim_mode: '-').result(binding)
83
- end
84
- end
85
- end
86
- end
@@ -1,150 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>S3arch Dashboard</title>
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <script>
7
- (function() {
8
- function getIdToken() {
9
- try {
10
- for (var i = 0; i < localStorage.length; i++) {
11
- var key = localStorage.key(i);
12
- if (key && key.indexOf('.idToken') !== -1) return localStorage.getItem(key);
13
- }
14
- } catch(e) {}
15
- return null;
16
- }
17
- function isTokenValid(token) {
18
- try {
19
- var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
20
- return payload.exp && payload.exp > Date.now() / 1000;
21
- } catch(e) { return false; }
22
- }
23
- var token = getIdToken();
24
- if (!token || !isTokenValid(token)) {
25
- window.location.replace('/login');
26
- }
27
- })();
28
- </script>
29
- <style>
30
- * { box-sizing: border-box; margin: 0; padding: 0; }
31
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 1.5rem; }
32
- h1 { margin-bottom: 1rem; font-size: 1.5rem; }
33
- .error { background: #fee; border: 1px solid #fcc; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; color: #c00; }
34
- .success { background: #efe; border: 1px solid #cfc; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; color: #060; display: none; }
35
- .meta { font-size: 0.85rem; color: #666; margin-bottom: 1rem; }
36
- .empty { padding: 2rem; text-align: center; color: #666; }
37
-
38
- /* Desktop: table */
39
- table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
40
- th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #eee; }
41
- th { background: #fafafa; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: #555; }
42
- tr:last-child td { border-bottom: none; }
43
- tr:hover td { background: #f9fafb; }
44
- .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: background 0.15s; }
45
- .btn-rebuild { background: #2563eb; color: #fff; }
46
- .btn-rebuild:hover { background: #1d4ed8; }
47
- .btn-rebuild:disabled { background: #93c5fd; cursor: not-allowed; }
48
- .owner-id { font-family: "SF Mono", SFMono-Regular, Consolas, monospace; font-size: 0.8rem; }
49
-
50
- /* Mobile: card layout */
51
- @media (max-width: 640px) {
52
- body { padding: 1rem; }
53
- table, thead, tbody, th, tr, td { display: block; }
54
- thead { display: none; }
55
- tr { background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 0.75rem; padding: 0.75rem; }
56
- tr:hover td { background: transparent; }
57
- td { padding: 0.25rem 0; border: none; display: flex; justify-content: space-between; align-items: center; }
58
- td::before { content: attr(data-label); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; color: #555; margin-right: 0.5rem; }
59
- td:last-child { margin-top: 0.5rem; justify-content: flex-start; }
60
- td:last-child::before { display: none; }
61
- .btn { width: 100%; text-align: center; padding: 0.6rem; }
62
- }
63
- </style>
64
- </head>
65
- <body>
66
- <h1>S3arch Dashboard</h1>
67
- <p class="meta"><%= @owners.size %> indexed owner<%= @owners.size == 1 ? '' : 's' %></p>
68
-
69
- <div id="flash" class="success"></div>
70
-
71
- <%- if @error -%>
72
- <div class="error"><strong>Error:</strong> <%= @error %></div>
73
- <%- end -%>
74
-
75
- <%- if @owners.any? -%>
76
- <table>
77
- <thead>
78
- <tr>
79
- <th>Owner</th>
80
- <th>Version</th>
81
- <th>Records</th>
82
- <th>Last Updated</th>
83
- <th>Actions</th>
84
- </tr>
85
- </thead>
86
- <tbody>
87
- <%- @owners.each do |owner| -%>
88
- <tr>
89
- <td class="owner-id" data-label="Owner"><%= owner[:owner_id] %></td>
90
- <td data-label="Version"><%= owner[:version].to_i %></td>
91
- <td data-label="Records"><%= owner[:record_count] ? owner[:record_count].to_i.to_s.gsub(/(\d)(?=(\d{3})+$)/, '\1,') : '—' %></td>
92
- <td data-label="Updated"><%= owner[:updated_at] || '—' %></td>
93
- <td>
94
- <button class="btn btn-rebuild" onclick="rebuild('<%= owner[:owner_id] %>', this)">Recreate SQLite</button>
95
- </td>
96
- </tr>
97
- <%- end -%>
98
- </tbody>
99
- </table>
100
- <%- else -%>
101
- <div class="empty">No indexed owners found.</div>
102
- <%- end -%>
103
-
104
- <script>
105
- function getIdToken() {
106
- try {
107
- for (var i = 0; i < localStorage.length; i++) {
108
- var key = localStorage.key(i);
109
- if (key && key.indexOf('.idToken') !== -1) return localStorage.getItem(key);
110
- }
111
- } catch(e) {}
112
- return null;
113
- }
114
-
115
- function rebuild(ownerId, btn) {
116
- btn.disabled = true;
117
- btn.textContent = 'Rebuilding\u2026';
118
- var headers = { 'Content-Type': 'application/json' };
119
- var token = getIdToken();
120
- if (token) headers['Authorization'] = 'Bearer ' + token;
121
-
122
- fetch(window.location.pathname.replace(/\/$/, '') + '/rebuild', {
123
- method: 'POST',
124
- headers: headers,
125
- body: JSON.stringify({ owner_id: ownerId })
126
- }).then(function(r) { return r.json(); }).then(function(data) {
127
- btn.textContent = 'Recreate SQLite';
128
- btn.disabled = false;
129
- var flash = document.getElementById('flash');
130
- if (data.status === 'ok') {
131
- flash.className = 'success';
132
- flash.textContent = 'Rebuild triggered for ' + ownerId;
133
- flash.style.display = 'block';
134
- } else {
135
- flash.className = 'error';
136
- flash.textContent = data.error || 'Rebuild failed';
137
- flash.style.display = 'block';
138
- }
139
- }).catch(function(e) {
140
- btn.textContent = 'Recreate SQLite';
141
- btn.disabled = false;
142
- var flash = document.getElementById('flash');
143
- flash.className = 'error';
144
- flash.textContent = 'Request failed: ' + e.message;
145
- flash.style.display = 'block';
146
- });
147
- }
148
- </script>
149
- </body>
150
- </html>
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'erb'
5
- require_relative 'dashboard/application'
6
-
7
- module S3arch
8
- module Dashboard
9
- def self.app
10
- Application.new
11
- end
12
- end
13
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module S3arch
4
- class Holster < Belt::Holster
5
- self.gem_root = File.expand_path('../..', __dir__)
6
- end
7
- end