scriptorium 0.0.3 → 0.6.1

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.
Files changed (292) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/back-icon.png +0 -0
  7. data/assets/icons/facebook.svg +1 -0
  8. data/assets/icons/github.svg +1 -0
  9. data/assets/icons/instagram.svg +1 -0
  10. data/assets/icons/reddit.svg +1 -0
  11. data/assets/icons/ui/.DS_Store +0 -0
  12. data/assets/icons/ui/back.png +0 -0
  13. data/assets/icons/ui/copy.png +0 -0
  14. data/assets/icons/ui/down.png +0 -0
  15. data/assets/icons/ui/end.png +0 -0
  16. data/assets/icons/ui/exit.png +0 -0
  17. data/assets/icons/ui/foo +10 -0
  18. data/assets/icons/ui/home.png +0 -0
  19. data/assets/icons/ui/left.png +0 -0
  20. data/assets/icons/ui/next.png +0 -0
  21. data/assets/icons/ui/right.png +0 -0
  22. data/assets/icons/ui/start.png +0 -0
  23. data/assets/icons/ui/up.png +0 -0
  24. data/assets/icons/x.svg +1 -0
  25. data/assets/icons/youtube.svg +1 -0
  26. data/assets/samples/placeholder.svg +9 -0
  27. data/assets/themes/standard/favicon.svg +6 -0
  28. data/bin/scriptorium +1511 -0
  29. data/doc/README.txt +6 -0
  30. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
  31. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
  32. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
  33. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
  34. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
  35. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
  36. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
  37. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  38. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
  39. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
  40. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  41. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
  42. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
  43. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
  44. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  45. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
  46. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
  47. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
  48. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
  49. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
  50. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
  51. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
  52. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
  53. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
  54. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
  55. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
  56. data/doc/anti-amnesia/20250807-213025.md +116 -0
  57. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
  58. data/doc/banner_svg_config.md +114 -0
  59. data/doc/contrib.lt3 +8 -0
  60. data/doc/dependencies.md +281 -0
  61. data/doc/hacker.lt3 +5 -0
  62. data/doc/reddit_credentials_template.json +8 -0
  63. data/doc/reddit_integration.md +207 -0
  64. data/doc/user.lt3 +38 -0
  65. data/doc/user_guide_section_1.md +137 -0
  66. data/doc/user_guide_section_10.md +515 -0
  67. data/doc/user_guide_section_11.md +708 -0
  68. data/doc/user_guide_section_2.md +233 -0
  69. data/doc/user_guide_section_3.md +5 -0
  70. data/doc/user_guide_section_4.md +221 -0
  71. data/doc/user_guide_section_5.md +243 -0
  72. data/doc/user_guide_section_6.md +147 -0
  73. data/doc/user_guide_section_7.md +311 -0
  74. data/doc/user_guide_section_8.md +224 -0
  75. data/doc/user_guide_section_9.md +375 -0
  76. data/doc/userdoc-toc.txt +88 -0
  77. data/lib/rouge/lexers/livetext.rb +74 -0
  78. data/lib/scriptorium/api.rb +640 -0
  79. data/lib/scriptorium/banner_svg.rb +742 -0
  80. data/lib/scriptorium/contract.rb +33 -0
  81. data/lib/scriptorium/exceptions.rb +170 -1
  82. data/lib/scriptorium/helpers.rb +475 -0
  83. data/lib/scriptorium/post.rb +195 -0
  84. data/lib/scriptorium/reddit.rb +83 -0
  85. data/lib/scriptorium/repo.rb +624 -0
  86. data/lib/scriptorium/standard_files.rb +515 -0
  87. data/lib/scriptorium/syntax_highlighter.rb +234 -0
  88. data/lib/scriptorium/theme.rb +179 -0
  89. data/lib/scriptorium/version.rb +2 -2
  90. data/lib/scriptorium/view.rb +976 -0
  91. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  92. data/lib/scriptorium/widgets/links.rb +112 -0
  93. data/lib/scriptorium/widgets/pages.rb +133 -0
  94. data/lib/scriptorium/widgets/widget.rb +133 -0
  95. data/lib/scriptorium.rb +21 -40
  96. data/lib/skeleton.rb +8 -2
  97. data/scriptorium.gemspec +15 -4
  98. data/test/README.md +69 -0
  99. data/test/all +43 -0
  100. data/test/api_demo.rb +99 -0
  101. data/test/assets/imagenotfound.jpg +0 -0
  102. data/test/assets/images/.DS_Store +0 -0
  103. data/test/assets/images/README.md +27 -0
  104. data/test/assets/images/odd_aspect.png +0 -0
  105. data/test/assets/images/perfect.png +0 -0
  106. data/test/assets/images/small.png +0 -0
  107. data/test/assets/images/tall.png +0 -0
  108. data/test/assets/images/very_tall.png +0 -0
  109. data/test/assets/images/very_wide.png +0 -0
  110. data/test/assets/images/wide.png +0 -0
  111. data/test/assets/testbanner.jpg +0 -0
  112. data/test/banner_svg/simple_helpers.rb +13 -0
  113. data/test/banner_svg/unit.rb +768 -0
  114. data/test/ed_test.rb +204 -0
  115. data/test/integration/cursor_banner_combinations.rb +193 -0
  116. data/test/integration/cursor_banner_features.rb +374 -0
  117. data/test/integration/integration_test.rb +326 -0
  118. data/test/livetext_plugin_test.rb +229 -0
  119. data/test/manual/asset_mgmt.rb +67 -0
  120. data/test/manual/banner-tests/config.txt +3 -0
  121. data/test/manual/banner-tests/index.html +45 -0
  122. data/test/manual/banner-tests/test01.html +58 -0
  123. data/test/manual/banner-tests/test02.html +58 -0
  124. data/test/manual/banner-tests/test03.html +58 -0
  125. data/test/manual/banner-tests/test04.html +65 -0
  126. data/test/manual/banner-tests/test05.html +65 -0
  127. data/test/manual/banner-tests/test06.html +65 -0
  128. data/test/manual/banner-tests/test07.html +65 -0
  129. data/test/manual/banner-tests/test08.html +59 -0
  130. data/test/manual/banner-tests/test09.html +59 -0
  131. data/test/manual/banner-tests/test10.html +59 -0
  132. data/test/manual/banner-tests/test11.html +59 -0
  133. data/test/manual/banner-tests/test12.html +59 -0
  134. data/test/manual/banner-tests/test13.html +59 -0
  135. data/test/manual/banner-tests/test14.html +59 -0
  136. data/test/manual/banner-tests/test15.html +58 -0
  137. data/test/manual/banner-tests/test16.html +58 -0
  138. data/test/manual/banner-tests/test17.html +58 -0
  139. data/test/manual/banner-tests/test18.html +68 -0
  140. data/test/manual/banner-tests/test19.html +68 -0
  141. data/test/manual/banner-tests/test20.html +68 -0
  142. data/test/manual/banner-tests/test21.html +68 -0
  143. data/test/manual/banner-tests/test22.html +68 -0
  144. data/test/manual/banner-tests/test23.html +68 -0
  145. data/test/manual/banner-tests/test24.html +68 -0
  146. data/test/manual/banner-tests/test25.html +67 -0
  147. data/test/manual/banner_environment.rb +192 -0
  148. data/test/manual/deploy_symlink_demo.rb +142 -0
  149. data/test/manual/environment.rb +67 -0
  150. data/test/manual/make_banner.rb +153 -0
  151. data/test/manual/sample_banner_config.txt +12 -0
  152. data/test/manual/symlink_demo.rb +117 -0
  153. data/test/manual/test1.rb +47 -0
  154. data/test/manual/test2.rb +12 -0
  155. data/test/manual/test3.rb +38 -0
  156. data/test/manual/test4.rb +40 -0
  157. data/test/manual/test5.rb +24 -0
  158. data/test/manual/test6.rb +73 -0
  159. data/test/manual/test_banner_combinations.rb +120 -0
  160. data/test/manual/test_banner_features.rb +306 -0
  161. data/test/manual/test_banner_from_file.rb +150 -0
  162. data/test/manual/test_banner_in_header.rb +35 -0
  163. data/test/manual/test_code_highlighting.rb +68 -0
  164. data/test/manual/test_complex_header.rb +74 -0
  165. data/test/manual/test_empty_header.rb +32 -0
  166. data/test/manual/test_radial_custom.rb +58 -0
  167. data/test/manual/test_radial_large_radius.rb +52 -0
  168. data/test/manual/test_svg_debug.rb +47 -0
  169. data/test/manual/test_syntax_highlighting.rb +147 -0
  170. data/test/pages-demo/config/currentview.txt +1 -0
  171. data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
  172. data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
  173. data/test/pages-demo/views/demo/config/common.js +57 -0
  174. data/test/pages-demo/views/demo/config/footer.txt +1 -0
  175. data/test/pages-demo/views/demo/config/global-head.txt +8 -0
  176. data/test/pages-demo/views/demo/config/header.txt +1 -0
  177. data/test/pages-demo/views/demo/config/layout.txt +1 -0
  178. data/test/pages-demo/views/demo/config/left.txt +1 -0
  179. data/test/pages-demo/views/demo/config/main.txt +1 -0
  180. data/test/pages-demo/views/demo/config/right.txt +1 -0
  181. data/test/pages-demo/views/demo/config.txt +3 -0
  182. data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
  183. data/test/pages-demo/views/demo/output/panes/header.html +1 -0
  184. data/test/pages-demo/views/demo/output/panes/left.html +1 -0
  185. data/test/pages-demo/views/demo/output/panes/main.html +1 -0
  186. data/test/pages-demo/views/demo/output/panes/right.html +1 -0
  187. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  188. data/test/rubytext/rubytext_demo_test.rb +42 -0
  189. data/test/rubytext/rubytext_testing_guide.md +277 -0
  190. data/test/run_automated_tests.rb +45 -0
  191. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
  192. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
  193. data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
  194. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
  195. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
  196. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
  197. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
  198. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
  199. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
  200. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
  201. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
  202. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
  203. data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
  204. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
  205. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
  206. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
  207. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
  208. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
  209. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
  210. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
  211. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
  212. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
  213. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
  214. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
  215. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
  216. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
  217. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
  218. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
  219. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
  220. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
  221. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
  222. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
  223. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
  224. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
  225. data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
  226. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
  227. data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
  228. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
  229. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
  230. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
  231. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
  232. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
  233. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
  234. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
  235. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
  236. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
  237. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
  238. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
  239. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
  240. data/test/staging/.DS_Store +0 -0
  241. data/test/syntax_highlighting_test.lt3 +124 -0
  242. data/test/test_helpers.rb +230 -0
  243. data/test/tui_editor_integration_test.rb +296 -0
  244. data/test/tui_integration_test.rb +637 -0
  245. data/test/unit/api.rb +1056 -0
  246. data/test/unit/asset_management.rb +245 -0
  247. data/test/unit/clipboard_test.rb +60 -0
  248. data/test/unit/contract_test.rb +91 -0
  249. data/test/unit/core.rb +857 -0
  250. data/test/unit/deploy_test.rb +187 -0
  251. data/test/unit/gem_asset_management.rb +189 -0
  252. data/test/unit/livetext_basic.rb +69 -0
  253. data/test/unit/livetext_compatibility.rb +89 -0
  254. data/test/unit/post.rb +244 -0
  255. data/test/unit/read_commented_file_test.rb +276 -0
  256. data/test/unit/reddit_test.rb +235 -0
  257. data/test/unit/repo.rb +548 -0
  258. data/test/unit/social_test.rb +369 -0
  259. data/test/unit/symlink_test.rb +213 -0
  260. data/test/unit/view.rb +431 -0
  261. data/test/unit/widgets.rb +669 -0
  262. data/test/wizard_test.rb +123 -0
  263. data/ui/README.md +67 -0
  264. data/ui/common/lib/ui_common.rb +8 -0
  265. data/ui/rubytext/README.md +191 -0
  266. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  267. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  268. data/ui/tui/bin/scriptorium +1420 -0
  269. data/ui/tui/test/tui_test.rb +23 -0
  270. data/ui/web/app/app.rb +1378 -0
  271. data/ui/web/app/error_helpers.rb +150 -0
  272. data/ui/web/app/views/advanced_config.erb +190 -0
  273. data/ui/web/app/views/asset_management.erb +589 -0
  274. data/ui/web/app/views/banner_config.erb +200 -0
  275. data/ui/web/app/views/configure_view.erb +401 -0
  276. data/ui/web/app/views/dashboard.erb +162 -0
  277. data/ui/web/app/views/deploy_config.erb +146 -0
  278. data/ui/web/app/views/edit_pages.erb +195 -0
  279. data/ui/web/app/views/edit_post.erb +54 -0
  280. data/ui/web/app/views/error_page.erb +29 -0
  281. data/ui/web/app/views/header_config.erb +155 -0
  282. data/ui/web/app/views/layout_config.erb +147 -0
  283. data/ui/web/app/views/navbar_config.erb +411 -0
  284. data/ui/web/app/views/view_dashboard.erb +138 -0
  285. data/ui/web/bin/scriptorium-web +153 -0
  286. data/ui/web/test/web_basic_test.rb +38 -0
  287. data/ui/web/test_navbar.txt +7 -0
  288. data/ui/web/tmp/web_server.log +5 -0
  289. data/ui/web/tmp/web_server.pid +1 -0
  290. metadata +359 -7
  291. data/lib/scriptorium/engine.rb +0 -22
  292. data/test/engine/unit.rb +0 -44
data/ui/web/app/app.rb ADDED
@@ -0,0 +1,1378 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra'
4
+ require 'sinatra/reloader' if development?
5
+ require 'fileutils'
6
+ require 'pathname'
7
+ begin
8
+ require 'fastimage'
9
+ rescue LoadError
10
+ # FastImage not available, will handle gracefully
11
+ end
12
+ require_relative '../../../lib/scriptorium'
13
+ require_relative 'error_helpers'
14
+
15
+ include ErrorHelpers
16
+
17
+ class ScriptoriumWeb < Sinatra::Base
18
+ set :port, 4567
19
+ set :bind, '0.0.0.0'
20
+ set :views, File.join(__dir__, 'views')
21
+ set :show_exceptions, false # Disable Sinatra's default error display
22
+
23
+ # Enable reloading in development
24
+ configure :development do
25
+ register Sinatra::Reloader
26
+ end
27
+
28
+ # Global error handler
29
+ error do
30
+ error_info = friendly_error_message(env['sinatra.error'])
31
+ @error = error_info[:message]
32
+ @suggestion = error_info[:suggestion]
33
+ erb :error_page
34
+ end
35
+
36
+ # Initialize API instance
37
+ before do
38
+ begin
39
+ @api = Scriptorium::API.new
40
+ # Use absolute path to the test repository
41
+ test_repo_path = File.join(__dir__, "..", "scriptorium-TEST")
42
+ @api.open_repo(test_repo_path) if Dir.exist?(test_repo_path)
43
+ rescue => e
44
+ @api = nil
45
+ end
46
+ end
47
+
48
+ # Main dashboard
49
+ get '/' do
50
+ @current_view = @api&.current_view
51
+ @views = @api&.views || []
52
+ begin
53
+ if @api&.instance_variable_get(:@repo)
54
+
55
+ # Only try to load posts if we have a current view
56
+ if @current_view
57
+ @posts = @api.posts(@current_view.name) || []
58
+ if @posts.length > 0
59
+ end
60
+ else
61
+ @posts = []
62
+ end
63
+ else
64
+ @posts = []
65
+ end
66
+ rescue => e
67
+ @posts = []
68
+ end
69
+ @error = @error || params[:error]
70
+ @message = params[:message]
71
+
72
+ erb :dashboard
73
+ end
74
+
75
+ # Change view
76
+ post '/change_view' do
77
+ view_name = params[:view_name]
78
+
79
+ if view_name.nil? || view_name.strip.empty?
80
+ redirect "/?error=No view selected"
81
+ return
82
+ end
83
+
84
+ begin
85
+ view = @api.lookup_view(view_name)
86
+ @api.view(view_name)
87
+ redirect '/?message=View changed successfully'
88
+ rescue => e
89
+ redirect "/?error=Failed to change view: #{e.message}"
90
+ end
91
+ end
92
+
93
+ # Create new repository
94
+ post '/create_repo' do
95
+ begin
96
+ @api.create_repo("scriptorium-TEST")
97
+ # After creating, open the repo
98
+ @api.open_repo("scriptorium-TEST")
99
+ redirect '/?message=Repository created successfully'
100
+ rescue => e
101
+ redirect "/?error=Failed to create repository: #{e.message}"
102
+ end
103
+ end
104
+
105
+ # Create new view
106
+ post '/create_view' do
107
+ begin
108
+ validate_required_params(params, :name, :title)
109
+
110
+ name = params[:name].strip
111
+ title = params[:title].strip
112
+ subtitle = params[:subtitle]&.strip || ""
113
+
114
+ @api.create_view(name, title, subtitle, theme: "standard")
115
+ redirect "/?message=View '#{name}' created successfully"
116
+ rescue => e
117
+ error_info = friendly_error_message(e)
118
+ redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
119
+ end
120
+ end
121
+
122
+ # Create new post
123
+ post '/create_post' do
124
+ begin
125
+ validate_required_params(params, :title)
126
+
127
+ current_view = @api&.current_view
128
+ if current_view.nil?
129
+ redirect "/?error=No view selected. Please select a view first."
130
+ return
131
+ end
132
+
133
+ # Create a draft first
134
+ draft_path = @api.create_draft(
135
+ title: params[:title].strip,
136
+ body: "", # Empty body to start
137
+ views: current_view.name,
138
+ tags: nil,
139
+ blurb: nil
140
+ )
141
+
142
+ # Convert draft to post immediately
143
+ post_num = @api.finish_draft(draft_path)
144
+ # Generate the post to create meta.txt and other files
145
+ begin
146
+ STDERR.puts "DEBUG: About to call generate_post for post #{post_num}"
147
+ STDERR.puts "DEBUG: Current working directory: #{Dir.pwd}"
148
+ STDERR.puts "DEBUG: API root: #{@api.root}"
149
+ @api.generate_post(post_num)
150
+ STDERR.puts "DEBUG: generate_post completed successfully"
151
+ # Check if meta.txt was created
152
+ meta_file = @api.root/"posts"/"#{post_num.to_s.rjust(4, '0')}"/"meta.txt"
153
+ STDERR.puts "DEBUG: Meta file path: #{meta_file}"
154
+ STDERR.puts "DEBUG: Meta file exists: #{File.exist?(meta_file)}"
155
+ redirect "/?message=Post '#{params[:title].strip}' created successfully (##{post_num})"
156
+ rescue => e
157
+ # Log the actual error for debugging
158
+ STDERR.puts "ERROR in generate_post: #{e.class}: #{e.message}"
159
+ STDERR.puts e.backtrace.join("\n")
160
+ error_info = friendly_error_message(e)
161
+ redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
162
+ end
163
+ rescue => e
164
+ error_info = friendly_error_message(e)
165
+ redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
166
+ end
167
+ end
168
+
169
+ # Edit post (redirects to file editing)
170
+ post '/edit_post' do
171
+ begin
172
+ validate_required_params(params, :post_id)
173
+
174
+ unless validate_post_id(params[:post_id])
175
+ redirect "/?error=Invalid post ID&suggestion=Please provide a valid post number."
176
+ return
177
+ end
178
+
179
+ post = @api.post(params[:post_id].to_i)
180
+ if post.nil?
181
+ redirect "/?error=Post not found&suggestion=The post may have been deleted or moved."
182
+ return
183
+ end
184
+
185
+ # Redirect to the edit page
186
+ redirect "/edit_post/#{params[:post_id]}"
187
+ rescue => e
188
+ error_info = friendly_error_message(e)
189
+ redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
190
+ end
191
+ end
192
+
193
+ # Show edit post page
194
+ get '/edit_post/:id' do
195
+ post_id = params[:id]&.to_i
196
+
197
+ if post_id.nil? || post_id <= 0
198
+ redirect "/?error=Invalid post ID"
199
+ return
200
+ end
201
+
202
+ begin
203
+ @post = @api.post(post_id)
204
+ if @post.nil?
205
+ redirect "/?error=Post not found"
206
+ return
207
+ end
208
+
209
+ # Read the source file content
210
+ source_file = @api.root/"posts"/@post.num/"source.lt3"
211
+ if File.exist?(source_file)
212
+ @content = File.read(source_file)
213
+ else
214
+ @content = "# #{@post.title}\n\n"
215
+ end
216
+
217
+ erb :edit_post
218
+ rescue => e
219
+ redirect "/?error=Failed to load post: #{e.message}"
220
+ end
221
+ end
222
+
223
+ # Save edited post
224
+ post '/save_post/:id' do
225
+ post_id = params[:id]&.to_i
226
+ content = params[:content]
227
+
228
+ if post_id.nil? || post_id <= 0
229
+ redirect "/?error=Invalid post ID"
230
+ return
231
+ end
232
+
233
+ if content.nil?
234
+ redirect "/edit_post/#{post_id}?error=No content provided"
235
+ return
236
+ end
237
+
238
+ begin
239
+ post = @api.post(post_id)
240
+ if post.nil?
241
+ redirect "/?error=Post not found"
242
+ return
243
+ end
244
+
245
+ # Write the content to the source file
246
+ source_file = @api.root/"posts"/post.num/"source.lt3"
247
+ File.write(source_file, content)
248
+
249
+ # Generate the post after saving
250
+ @api.generate_post(post_id)
251
+
252
+ redirect "/?message=Post ##{post_id} saved and generated successfully"
253
+ rescue => e
254
+ redirect "/edit_post/#{post_id}?error=Failed to save post: #{e.message}"
255
+ end
256
+ end
257
+
258
+ # Generate post
259
+ post '/generate_post' do
260
+ post_id = params[:post_id]&.to_i
261
+
262
+ if post_id.nil? || post_id <= 0
263
+ redirect "/?error=Invalid post ID"
264
+ return
265
+ end
266
+
267
+ begin
268
+ post = @api.post(post_id)
269
+ if post.nil?
270
+ redirect "/?error=Post not found"
271
+ return
272
+ end
273
+
274
+ # Generate the post
275
+ @api.generate_post(post_id)
276
+ redirect "/?message=Post ##{post_id} generated successfully"
277
+ rescue => e
278
+ redirect "/?error=Failed to generate post: #{e.message}"
279
+ end
280
+ end
281
+
282
+ # Generate view
283
+ post '/generate_view' do
284
+ view_name = params[:view_name]
285
+
286
+ begin
287
+ if view_name.nil? || view_name.strip.empty?
288
+ redirect "/?error=No view specified"
289
+ return
290
+ end
291
+
292
+ # Generate the view
293
+ @api.generate_view(view_name)
294
+ redirect "/?message=View '#{view_name}' generated successfully"
295
+ rescue => e
296
+ redirect "/?error=Failed to generate view: #{e.message}"
297
+ end
298
+ end
299
+
300
+ # Preview view
301
+ post '/preview_view' do
302
+ view_name = params[:view_name]
303
+
304
+ begin
305
+ if view_name.nil? || view_name.strip.empty?
306
+ redirect "/?error=No view specified"
307
+ return
308
+ end
309
+
310
+ # Generate the view first to ensure it's up to date
311
+ @api.generate_view(view_name)
312
+
313
+ # Redirect to the generated index.html file
314
+ view_dir = @api.root/"views"/view_name
315
+ index_file = view_dir/"output"/"index.html"
316
+
317
+ if File.exist?(index_file)
318
+ # Return the HTML content directly for preview
319
+ content_type :html
320
+ File.read(index_file)
321
+ else
322
+ redirect "/?error=Preview file not found - view may not have been generated properly"
323
+ end
324
+ rescue => e
325
+ redirect "/?error=Failed to preview view: #{e.message}"
326
+ end
327
+ end
328
+
329
+ # Serve post files for preview
330
+ get '/preview/:view_name/posts/:filename' do
331
+ view_name = params[:view_name]
332
+ filename = params[:filename]
333
+
334
+ STDERR.puts "DEBUG: Preview request - view_name: #{view_name}, filename: #{filename}"
335
+
336
+ begin
337
+ if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
338
+ STDERR.puts "DEBUG: Missing parameters"
339
+ status 404
340
+ return "File not found"
341
+ end
342
+
343
+ # Construct the file path
344
+ post_file = @api.root/"views"/view_name/"output"/"posts"/filename
345
+ STDERR.puts "DEBUG: Looking for file: #{post_file}"
346
+ STDERR.puts "DEBUG: File exists: #{File.exist?(post_file)}"
347
+
348
+ if File.exist?(post_file)
349
+ content_type :html
350
+ File.read(post_file)
351
+ else
352
+ STDERR.puts "DEBUG: File not found"
353
+ status 404
354
+ "File not found: #{filename}"
355
+ end
356
+ rescue => e
357
+ STDERR.puts "DEBUG: Error: #{e.message}"
358
+ status 500
359
+ "Error loading file: #{e.message}"
360
+ end
361
+ end
362
+
363
+ # Show view configuration page
364
+ get '/configure_view/:name' do
365
+ begin
366
+ validate_required_params(params, :name)
367
+
368
+ unless validate_view_name(params[:name])
369
+ redirect "/?error=Invalid view name&suggestion=View names must contain only letters, numbers, hyphens, and underscores."
370
+ return
371
+ end
372
+
373
+ view = @api.lookup_view(params[:name])
374
+ if view.nil?
375
+ redirect "/?error=View not found&suggestion=The view '#{params[:name]}' does not exist. Check the view name or create it first."
376
+ return
377
+ end
378
+
379
+ @view = view
380
+
381
+ # Load view configuration safely
382
+ config_file = @api.root/"views"/params[:name]/"config.txt"
383
+ @config_content = safe_read_file(config_file, "# View configuration for #{params[:name]}\n")
384
+
385
+ # Load layout file safely
386
+ layout_file = @api.root/"views"/params[:name]/"config"/"layout.txt"
387
+ @layout_content = safe_read_file(layout_file, "# Layout configuration for #{params[:name]}\n")
388
+
389
+ erb :configure_view
390
+ rescue => e
391
+ error_info = friendly_error_message(e)
392
+ redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
393
+ end
394
+ end
395
+
396
+ # Save view configuration
397
+ post '/save_view_config/:name' do
398
+ view_name = params[:name]
399
+
400
+ begin
401
+ view = @api.lookup_view(view_name)
402
+ if view.nil?
403
+ redirect "/?error=View not found"
404
+ return
405
+ end
406
+
407
+ # Step 1: Save basic view information
408
+ if params[:view_title] && params[:view_subtitle] && params[:view_theme]
409
+ config_content = "title #{params[:view_title]}\n"
410
+ config_content += "subtitle #{params[:view_subtitle]}\n"
411
+ config_content += "theme #{params[:view_theme]}\n"
412
+
413
+ config_file = @api.root/"views"/view_name/"config.txt"
414
+ File.write(config_file, config_content)
415
+ end
416
+
417
+ # Step 2: Save layout configuration
418
+ if params[:containers]
419
+ layout_content = ""
420
+ containers = Array(params[:containers])
421
+
422
+ containers.each do |container|
423
+ case container
424
+ when 'header'
425
+ layout_content += "header # Top (banner? title? navbar? etc.)\n"
426
+ when 'left'
427
+ width = params[:left_width] || "15%"
428
+ layout_content += "left #{width} # Left sidebar, #{width} width\n"
429
+ when 'main'
430
+ layout_content += "main # Main (center) container - posts/etc.\n"
431
+ when 'right'
432
+ width = params[:right_width] || "15%"
433
+ layout_content += "right #{width} # Right sidebar, #{width} width\n"
434
+ when 'footer'
435
+ layout_content += "footer # Footer (copyright? mail? social media? etc.)\n"
436
+ end
437
+ end
438
+
439
+ layout_file = @api.root/"views"/view_name/"config"/"layout.txt"
440
+ FileUtils.mkdir_p(File.dirname(layout_file))
441
+ File.write(layout_file, layout_content)
442
+ end
443
+
444
+ # Step 3: Save container content files
445
+ containers = Array(params[:containers])
446
+
447
+ containers.each do |container|
448
+ content_param = "#{container}_content"
449
+ if params[content_param]
450
+ content_file = @api.root/"views"/view_name/"config"/"#{container}.txt"
451
+ FileUtils.mkdir_p(File.dirname(content_file))
452
+ File.write(content_file, params[content_param])
453
+
454
+ # If this is header with "banner svg", create default svg.txt
455
+ if container == 'header' && params[content_param].strip == 'banner svg'
456
+ svg_file = @api.root/"views"/view_name/"config"/"svg.txt"
457
+ unless File.exist?(svg_file)
458
+ # Create default SVG configuration
459
+ default_svg_content = "# SVG Banner Configuration\n"
460
+ default_svg_content += "# Light gradient background with dark text\n"
461
+ default_svg_content += "back.linear #f8f9fa #e9ecef lr\n"
462
+ default_svg_content += "text.color #374151\n"
463
+ default_svg_content += "title.style bold\n"
464
+ File.write(svg_file, default_svg_content)
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ redirect "/?message=View '#{view_name}' configuration saved successfully"
471
+ rescue => e
472
+ redirect "/configure_view/#{view_name}?error=Failed to save configuration: #{e.message}"
473
+ end
474
+ end
475
+
476
+ # Banner configuration page
477
+ get '/banner_config' do
478
+ @current_view = @api&.current_view
479
+ if @current_view.nil?
480
+ redirect "/?error=No view selected. Please select a view first."
481
+ return
482
+ end
483
+
484
+ # Get current SVG config
485
+ svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
486
+ @svg_config = File.exist?(svg_file) ? File.read(svg_file) : ""
487
+
488
+ # Generate current banner for display
489
+ begin
490
+ banner = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
491
+
492
+ # Use the same approach as View class
493
+ if @svg_config.strip.length > 0
494
+ svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
495
+ banner.parse_header_svg(svg_file)
496
+ else
497
+ # No config, use defaults
498
+ banner.parse_header_svg
499
+ end
500
+
501
+ @banner_svg = banner.generate_svg
502
+ rescue => e
503
+ @banner_svg = "<p>Error generating banner: #{e.message}</p>"
504
+ end
505
+
506
+ erb :banner_config
507
+ end
508
+
509
+ # Update banner configuration
510
+ post '/banner_config' do
511
+ @current_view = @api&.current_view
512
+ if @current_view.nil?
513
+ redirect "/?error=No view selected. Please select a view first."
514
+ return
515
+ end
516
+
517
+ begin
518
+ svg_config = params[:svg_config] || ""
519
+
520
+ # Save the SVG configuration
521
+ svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
522
+ FileUtils.mkdir_p(File.dirname(svg_file))
523
+ File.write(svg_file, svg_config)
524
+
525
+ # Update status
526
+ update_config_status(@current_view.name, "banner", true)
527
+
528
+ redirect "/banner_config?message=Banner configuration updated successfully"
529
+ rescue => e
530
+ redirect "/banner_config?error=Failed to save banner configuration: #{e.message}"
531
+ end
532
+ end
533
+
534
+ # Navbar configuration page
535
+ get '/navbar_config' do
536
+ @current_view = @api&.current_view
537
+ if @current_view.nil?
538
+ redirect "/?error=No view selected. Please select a view first."
539
+ return
540
+ end
541
+
542
+ # Get current navbar config
543
+ navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
544
+ @navbar_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
545
+
546
+ # Generate current navbar preview
547
+ begin
548
+ view = @api.lookup_view(@current_view.name)
549
+ @navbar_preview = view.build_nav(nil) # nil = use default navbar.txt
550
+ rescue => e
551
+ @navbar_preview = "<p>Error generating navbar: #{e.message}</p>"
552
+ end
553
+
554
+ erb :navbar_config
555
+ end
556
+
557
+ # Add item (top-level link or parent)
558
+ post '/navbar_config/add_item' do
559
+ @current_view = @api&.current_view
560
+ if @current_view.nil?
561
+ redirect "/?error=No view selected. Please select a view first."
562
+ return
563
+ end
564
+
565
+ begin
566
+ label = params[:label]&.strip
567
+ filename = params[:filename]&.strip
568
+ action = params[:action]
569
+
570
+ if label.nil? || label.empty?
571
+ redirect "/navbar_config?error=Label is required"
572
+ return
573
+ end
574
+
575
+ # Read current navbar config
576
+ navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
577
+ current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
578
+
579
+ # Add new item based on action
580
+ if action == "link"
581
+ if filename.nil? || filename.empty?
582
+ redirect "/navbar_config?error=Filename is required for top-level links"
583
+ return
584
+ end
585
+ new_line = "-#{label} #{filename}"
586
+ message = "Added #{label} as top-level link"
587
+ else
588
+ new_line = "=#{label}"
589
+ message = "Added #{label} as parent"
590
+ end
591
+
592
+ # Append to config
593
+ updated_config = current_config.empty? ? new_line : "#{current_config.rstrip}\n#{new_line}"
594
+
595
+ # Save the updated configuration
596
+ FileUtils.mkdir_p(File.dirname(navbar_file))
597
+ File.write(navbar_file, updated_config.rstrip + "\n")
598
+
599
+ redirect "/navbar_config?message=#{message}"
600
+ rescue => e
601
+ redirect "/navbar_config?error=Failed to add item: #{e.message}"
602
+ end
603
+ end
604
+
605
+ # Add child to parent
606
+ post '/navbar_config/add_child' do
607
+ @current_view = @api&.current_view
608
+ if @current_view.nil?
609
+ redirect "/?error=No view selected. Please select a view first."
610
+ return
611
+ end
612
+
613
+ begin
614
+ parent = params[:parent]&.strip
615
+ label = params[:label]&.strip
616
+ filename = params[:filename]&.strip
617
+
618
+ if parent.nil? || parent.empty?
619
+ redirect "/navbar_config?error=Parent is required"
620
+ return
621
+ end
622
+
623
+ if label.nil? || label.empty?
624
+ redirect "/navbar_config?error=Label is required"
625
+ return
626
+ end
627
+
628
+ if filename.nil? || filename.empty?
629
+ redirect "/navbar_config?error=Filename is required"
630
+ return
631
+ end
632
+
633
+ # Read current navbar config
634
+ navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
635
+ current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
636
+
637
+ # Find the parent and add child after it
638
+ lines = current_config.lines
639
+ new_lines = []
640
+ parent_found = false
641
+
642
+ lines.each do |line|
643
+ new_lines << line
644
+ if line.strip == "=#{parent}"
645
+ parent_found = true
646
+ # Add child on next line
647
+ new_lines << " #{label} #{filename}\n"
648
+ end
649
+ end
650
+
651
+ if !parent_found
652
+ redirect "/navbar_config?error=Parent '#{parent}' not found"
653
+ return
654
+ end
655
+
656
+ # Save the updated configuration
657
+ FileUtils.mkdir_p(File.dirname(navbar_file))
658
+ File.write(navbar_file, new_lines.join.rstrip + "\n")
659
+
660
+ redirect "/navbar_config?message=Added #{label} as child of #{parent}"
661
+ rescue => e
662
+ redirect "/navbar_config?error=Failed to add child: #{e.message}"
663
+ end
664
+ end
665
+
666
+ # Save direct edit of navbar config
667
+ post '/navbar_config/save_direct' do
668
+ @current_view = @api&.current_view
669
+ if @current_view.nil?
670
+ redirect "/?error=No view selected. Please select a view first."
671
+ return
672
+ end
673
+
674
+ begin
675
+ config = params[:config]&.strip
676
+ if config.nil?
677
+ redirect "/navbar_config?error=Configuration is required"
678
+ return
679
+ end
680
+
681
+ # Save the configuration
682
+ navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
683
+ FileUtils.mkdir_p(File.dirname(navbar_file))
684
+ File.write(navbar_file, config.rstrip + "\n")
685
+
686
+ # Check for missing pages and create them
687
+ pages_created = []
688
+ pages_dir = @api.root/"views"/@current_view.name/"pages"
689
+ FileUtils.mkdir_p(pages_dir) unless Dir.exist?(pages_dir)
690
+
691
+ # Parse navbar config to find page filenames
692
+ config.lines.each do |line|
693
+ line = line.rstrip
694
+ next if line.empty? || line.start_with?('#')
695
+
696
+ # Check for top-level links (start with -)
697
+ if line.start_with?('-')
698
+ if line.include?(' ')
699
+ parts = line.split(/\s{2,}/, 2)
700
+ if parts.length >= 2
701
+ title = parts[0].strip
702
+ filename = parts[1].strip
703
+ next if filename.empty?
704
+
705
+ # Add .lt3 extension if no extension
706
+ filename += '.lt3' unless filename.include?('.')
707
+
708
+ # Check if page exists
709
+ page_file = pages_dir/filename
710
+ unless File.exist?(page_file)
711
+ content = ".page_title #{title}\n\n"
712
+ File.write(page_file, content)
713
+ pages_created << filename
714
+ end
715
+ end
716
+ end
717
+ # Check for child links (start with space)
718
+ elsif line.start_with?(' ')
719
+ if line.include?(' ')
720
+ parts = line.split(/\s{2,}/, 2)
721
+ if parts.length >= 2
722
+ title = parts[0].strip
723
+ filename = parts[1].strip
724
+ next if filename.empty?
725
+
726
+ # Add .lt3 extension if no extension
727
+ filename += '.lt3' unless filename.include?('.')
728
+
729
+ # Check if page exists
730
+ page_file = pages_dir/filename
731
+ unless File.exist?(page_file)
732
+ content = ".page_title #{title}\n\n"
733
+ File.write(page_file, content)
734
+ pages_created << filename
735
+ end
736
+ end
737
+ end
738
+ end
739
+ end
740
+
741
+ # Build success message
742
+ message = "Configuration saved successfully"
743
+ if pages_created.any?
744
+ message += ". Created missing pages: #{pages_created.join(', ')}"
745
+ end
746
+
747
+ redirect "/navbar_config?message=#{message}"
748
+ rescue => e
749
+ redirect "/navbar_config?error=Failed to save configuration: #{e.message}"
750
+ end
751
+ end
752
+
753
+ # Edit pages page
754
+ get '/edit_pages' do
755
+ @current_view = @api&.current_view
756
+ if @current_view.nil?
757
+ redirect "/?error=No view selected. Please select a view first."
758
+ return
759
+ end
760
+
761
+ # Get all pages in the current view
762
+ pages_dir = @api.root/"views"/@current_view.name/"pages"
763
+ @pages = []
764
+
765
+ if Dir.exist?(pages_dir)
766
+ Dir.glob(pages_dir/"*").each do |file|
767
+ next unless File.file?(file)
768
+ filename = File.basename(file)
769
+ content = File.read(file)
770
+
771
+ # Extract page title from .page_title directive
772
+ title = nil
773
+ if content.lines.first&.strip&.start_with?('.page_title')
774
+ title = content.lines.first.strip.sub('.page_title', '').strip
775
+ end
776
+
777
+ @pages << {
778
+ filename: filename,
779
+ title: title,
780
+ content: content,
781
+ empty: content.strip.empty?
782
+ }
783
+ end
784
+ end
785
+
786
+ # Sort pages alphabetically
787
+ @pages.sort_by! { |page| page[:filename] }
788
+
789
+ erb :edit_pages
790
+ end
791
+
792
+ # Save page content
793
+ post '/edit_pages/save' do
794
+ @current_view = @api&.current_view
795
+ if @current_view.nil?
796
+ redirect "/?error=No view selected. Please select a view first."
797
+ return
798
+ end
799
+
800
+ begin
801
+ filename = params[:filename]&.strip
802
+ content = params[:content]&.strip || ""
803
+
804
+ if filename.nil? || filename.empty?
805
+ redirect "/edit_pages?error=Filename is required"
806
+ return
807
+ end
808
+
809
+ # Save the page
810
+ pages_dir = @api.root/"views"/@current_view.name/"pages"
811
+ FileUtils.mkdir_p(pages_dir)
812
+ page_file = pages_dir/filename
813
+ File.write(page_file, content)
814
+
815
+ redirect "/edit_pages?message=Page '#{filename}' saved successfully"
816
+ rescue => e
817
+ redirect "/edit_pages?error=Failed to save page: #{e.message}"
818
+ end
819
+ end
820
+
821
+ # Per-view dashboard
822
+ get '/view/:name' do
823
+ view_name = params[:name]
824
+
825
+ begin
826
+ # Look up the view
827
+ view = @api.lookup_view(view_name)
828
+ if view.nil?
829
+ redirect "/?error=View '#{view_name}' not found"
830
+ return
831
+ end
832
+
833
+ # Set as current view
834
+ @api.view(view_name)
835
+ @current_view = @api.current_view
836
+
837
+ # Generate banner for display
838
+ begin
839
+ bsvg = Scriptorium::BannerSVG.new(view.title, view.subtitle)
840
+ svg_config_file = @api.root/"views"/view_name/"config"/"svg.txt"
841
+ if File.exist?(svg_config_file)
842
+ # Temporarily rename svg.txt to config.txt for BannerSVG compatibility
843
+ config_dir = @api.root/"views"/view_name/"config"
844
+ Dir.chdir(config_dir) do
845
+ if File.exist?("config.txt")
846
+ File.rename("config.txt", "config.txt.backup")
847
+ end
848
+ File.rename("svg.txt", "config.txt")
849
+
850
+ begin
851
+ bsvg.parse_header_svg
852
+ ensure
853
+ # Restore original files
854
+ File.rename("config.txt", "svg.txt")
855
+ if File.exist?("config.txt.backup")
856
+ File.rename("config.txt.backup", "config.txt")
857
+ end
858
+ end
859
+ end
860
+ else
861
+ bsvg.parse_header_svg
862
+ end
863
+ # Generate responsive SVG for web display
864
+ svg_html = bsvg.generate_svg
865
+ # Extract the SVG element and make it responsive
866
+ svg_match = svg_html.match(/<svg[^>]*>(.*)<\/svg>/m)
867
+ if svg_match
868
+ svg_content = svg_match[1]
869
+ # Calculate height based on aspect ratio (7.0 from config)
870
+ width = 800
871
+ height = (width / 7.0).to_i
872
+ @banner_svg = <<~HTML
873
+ <svg xmlns='http://www.w3.org/2000/svg'
874
+ width='100%' height='auto'
875
+ viewBox='0 0 #{width} #{height}'
876
+ preserveAspectRatio='xMidYMid meet'>
877
+ #{svg_content}
878
+ </svg>
879
+ HTML
880
+ else
881
+ @banner_svg = svg_html
882
+ end
883
+ rescue => e
884
+ @banner_svg = "<p>Error generating banner: #{e.message}</p>"
885
+ end
886
+
887
+ erb :view_dashboard
888
+ rescue => e
889
+ redirect "/?error=Failed to load view dashboard: #{e.message}"
890
+ end
891
+ end
892
+
893
+ # Advanced configuration page
894
+ get '/advanced_config' do
895
+ @current_view = @api&.current_view
896
+ if @current_view.nil?
897
+ redirect "/?error=No view selected. Please select a view first."
898
+ return
899
+ end
900
+
901
+ # Read status from status.txt file
902
+ config_dir = @api.root/"views"/@current_view.name/"config"
903
+ status_file = config_dir/"status.txt"
904
+ @configs = {}
905
+
906
+ if File.exist?(status_file)
907
+ status_content = File.read(status_file)
908
+ status_content.lines.each do |line|
909
+ line = line.strip
910
+ next if line.empty? || line.start_with?('#')
911
+ if line.include?(' ')
912
+ key, value = line.split(/\s+/, 2)
913
+ @configs[key.to_sym] = value == 'y'
914
+ end
915
+ end
916
+ else
917
+ # Default to all 'n' if status file doesn't exist
918
+ @configs = {
919
+ header: false,
920
+ banner: false,
921
+ navbar: false,
922
+ left: false,
923
+ right: false,
924
+ pages: false,
925
+ deploy: false
926
+ }
927
+ end
928
+
929
+ # Read layout to determine which containers exist
930
+ layout_file = config_dir/"layout.txt"
931
+ @layout_containers = []
932
+ if File.exist?(layout_file)
933
+ layout_content = File.read(layout_file)
934
+ layout_content.lines.each do |line|
935
+ line = line.strip
936
+ next if line.empty? || line.start_with?('#')
937
+ if line.include?(' ')
938
+ container = line.split(/\s+/, 2)[0]
939
+ @layout_containers << container
940
+ else
941
+ @layout_containers << line
942
+ end
943
+ end
944
+ end
945
+
946
+ erb :advanced_config
947
+ end
948
+
949
+ # Header configuration page
950
+ get '/header_config' do
951
+ @current_view = @api&.current_view
952
+ if @current_view.nil?
953
+ redirect "/?error=No view selected. Please select a view first."
954
+ return
955
+ end
956
+
957
+ # Read current header config
958
+ header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
959
+ @current_config = ""
960
+ if File.exist?(header_file)
961
+ @current_config = File.read(header_file).strip
962
+ end
963
+
964
+ # Parse current settings
965
+ @banner_type = @current_config.include?("banner svg") ? "svg" : "image"
966
+ @navbar_enabled = @current_config.include?("navbar")
967
+
968
+ erb :header_config
969
+ end
970
+
971
+ # Update header configuration
972
+ post '/header_config' do
973
+ @current_view = @api&.current_view
974
+ if @current_view.nil?
975
+ redirect "/?error=No view selected. Please select a view first."
976
+ return
977
+ end
978
+
979
+ begin
980
+ banner_type = params[:banner_type] || "svg"
981
+ navbar_enabled = params[:navbar_enabled] == "1"
982
+
983
+ # Build header.txt content
984
+ header_content = []
985
+ header_content << "banner #{banner_type}"
986
+ header_content << "navbar" if navbar_enabled
987
+
988
+ # Save the header configuration
989
+ header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
990
+ FileUtils.mkdir_p(File.dirname(header_file))
991
+ File.write(header_file, header_content.join("\n") + "\n")
992
+
993
+ # Update status
994
+ update_config_status(@current_view.name, "header", true)
995
+
996
+ redirect "/advanced_config?message=Header configuration updated successfully"
997
+ rescue => e
998
+ redirect "/header_config?error=Failed to save header configuration: #{e.message}"
999
+ end
1000
+ end
1001
+
1002
+ # Deployment configuration page
1003
+ get '/deploy_config' do
1004
+ @current_view = @api&.current_view
1005
+ if @current_view.nil?
1006
+ redirect "/?error=No view selected. Please select a view first."
1007
+ return
1008
+ end
1009
+
1010
+ # Read current deployment config
1011
+ deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
1012
+ @deploy_config = ""
1013
+ if File.exist?(deploy_file)
1014
+ @deploy_config = File.read(deploy_file).strip
1015
+ end
1016
+
1017
+ erb :deploy_config
1018
+ end
1019
+
1020
+ # Update deployment configuration
1021
+ post '/deploy_config' do
1022
+ @current_view = @api&.current_view
1023
+ if @current_view.nil?
1024
+ redirect "/?error=No view selected. Please select a view first."
1025
+ return
1026
+ end
1027
+
1028
+ begin
1029
+ deploy_config = params[:deploy_config] || ""
1030
+
1031
+ # Save the deployment configuration
1032
+ deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
1033
+ FileUtils.mkdir_p(File.dirname(deploy_file))
1034
+ File.write(deploy_file, deploy_config + "\n")
1035
+
1036
+ # Update status
1037
+ update_config_status(@current_view.name, "deploy", true)
1038
+
1039
+ redirect "/advanced_config?message=Deployment configuration updated successfully"
1040
+ rescue => e
1041
+ redirect "/deploy_config?error=Failed to save deployment configuration: #{e.message}"
1042
+ end
1043
+ end
1044
+
1045
+ # Layout configuration page
1046
+ get '/layout_config' do
1047
+ @current_view = @api&.current_view
1048
+ if @current_view.nil?
1049
+ redirect "/?error=No view selected. Please select a view first."
1050
+ return
1051
+ end
1052
+
1053
+ # Read current layout config
1054
+ layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
1055
+ @layout_config = ""
1056
+ if File.exist?(layout_file)
1057
+ @layout_config = File.read(layout_file).strip
1058
+ end
1059
+
1060
+ erb :layout_config
1061
+ end
1062
+
1063
+ # Update layout configuration
1064
+ post '/layout_config' do
1065
+ @current_view = @api&.current_view
1066
+ if @current_view.nil?
1067
+ redirect "/?error=No view selected. Please select a view first."
1068
+ return
1069
+ end
1070
+
1071
+ begin
1072
+ layout_config = params[:layout_config] || ""
1073
+
1074
+ # Save the layout configuration
1075
+ layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
1076
+ FileUtils.mkdir_p(File.dirname(layout_file))
1077
+ File.write(layout_file, layout_config + "\n")
1078
+
1079
+ redirect "/advanced_config?message=Layout configuration updated successfully"
1080
+ rescue => e
1081
+ redirect "/layout_config?error=Failed to save layout configuration: #{e.message}"
1082
+ end
1083
+ end
1084
+
1085
+ # Serve global assets
1086
+ get '/assets/*' do
1087
+ asset_path = params[:splat].first
1088
+ asset_file = @api.root/"assets"/asset_path
1089
+
1090
+ if File.exist?(asset_file) && File.file?(asset_file)
1091
+ send_file asset_file
1092
+ else
1093
+ status 404
1094
+ "Asset not found"
1095
+ end
1096
+ end
1097
+
1098
+ # Serve view-specific assets
1099
+ get '/views/:view_name/assets/*' do
1100
+ view_name = params[:view_name]
1101
+ asset_path = params[:splat].first
1102
+ asset_file = @api.root/"views"/view_name/"assets"/asset_path
1103
+
1104
+ if File.exist?(asset_file) && File.file?(asset_file)
1105
+ send_file asset_file
1106
+ else
1107
+ status 404
1108
+ "Asset not found"
1109
+ end
1110
+ end
1111
+
1112
+ # Server status endpoint
1113
+ get '/status' do
1114
+ content_type :json
1115
+ {
1116
+ status: 'running',
1117
+ port: settings.port,
1118
+ current_view: @api.current_view&.name,
1119
+ repo_loaded: !@api.instance_variable_get(:@repo).nil?
1120
+ }.to_json
1121
+ end
1122
+
1123
+ # Asset management page
1124
+ get '/asset_management' do
1125
+ @current_view = @api&.current_view
1126
+ if @current_view.nil?
1127
+ redirect "/?error=No view selected. Please select a view first."
1128
+ return
1129
+ end
1130
+
1131
+ # Get global assets
1132
+ global_assets_dir = @api.root/"assets"
1133
+ @global_assets = []
1134
+ @library_assets = []
1135
+
1136
+ if Dir.exist?(global_assets_dir)
1137
+ Dir.glob(global_assets_dir/"*").each do |file|
1138
+ next unless File.file?(file)
1139
+ filename = File.basename(file)
1140
+ size = File.size(file)
1141
+ dimensions = get_image_dimensions(file)
1142
+ @global_assets << {
1143
+ filename: filename,
1144
+ size: size,
1145
+ path: file,
1146
+ dimensions: dimensions
1147
+ }
1148
+ end
1149
+
1150
+ # Get library assets
1151
+ library_dir = global_assets_dir/"library"
1152
+ if Dir.exist?(library_dir)
1153
+ Dir.glob(library_dir/"*").each do |file|
1154
+ next unless File.file?(file)
1155
+ filename = File.basename(file)
1156
+ size = File.size(file)
1157
+ dimensions = get_image_dimensions(file)
1158
+ @library_assets << {
1159
+ filename: filename,
1160
+ size: size,
1161
+ path: file,
1162
+ dimensions: dimensions
1163
+ }
1164
+ end
1165
+ end
1166
+ end
1167
+
1168
+ # Get view-specific assets
1169
+ view_assets_dir = @api.root/"views"/@current_view.name/"assets"
1170
+ @view_assets = []
1171
+
1172
+ if Dir.exist?(view_assets_dir)
1173
+ Dir.glob(view_assets_dir/"*").each do |file|
1174
+ next unless File.file?(file)
1175
+ filename = File.basename(file)
1176
+ size = File.size(file)
1177
+ dimensions = get_image_dimensions(file)
1178
+ @view_assets << {
1179
+ filename: filename,
1180
+ size: size,
1181
+ path: file,
1182
+ dimensions: dimensions
1183
+ }
1184
+ end
1185
+ end
1186
+
1187
+ # Sort all asset lists
1188
+ @global_assets.sort_by! { |asset| asset[:filename] }
1189
+ @library_assets.sort_by! { |asset| asset[:filename] }
1190
+ @view_assets.sort_by! { |asset| asset[:filename] }
1191
+
1192
+ erb :asset_management
1193
+ end
1194
+
1195
+ # Upload asset
1196
+ post '/asset_management/upload' do
1197
+ @current_view = @api&.current_view
1198
+ if @current_view.nil?
1199
+ redirect "/?error=No view selected. Please select a view first."
1200
+ return
1201
+ end
1202
+
1203
+ begin
1204
+ target = params[:target] # 'global', 'library', or 'view'
1205
+ file = params[:file]
1206
+
1207
+ if file.nil? || file[:tempfile].nil?
1208
+ redirect "/asset_management?error=No file selected"
1209
+ return
1210
+ end
1211
+
1212
+ filename = file[:filename]
1213
+ tempfile = file[:tempfile]
1214
+
1215
+ # Determine target directory
1216
+ case target
1217
+ when 'global'
1218
+ target_dir = @api.root/"assets"
1219
+ when 'library'
1220
+ target_dir = @api.root/"assets"/"library"
1221
+ when 'view'
1222
+ target_dir = @api.root/"views"/@current_view.name/"assets"
1223
+ else
1224
+ redirect "/asset_management?error=Invalid target"
1225
+ return
1226
+ end
1227
+
1228
+ # Create directory if it doesn't exist
1229
+ FileUtils.mkdir_p(target_dir)
1230
+
1231
+ # Save the file
1232
+ target_file = target_dir/filename
1233
+ FileUtils.cp(tempfile.path, target_file)
1234
+
1235
+ redirect "/asset_management?message=Asset '#{filename}' uploaded successfully to #{target}"
1236
+ rescue => e
1237
+ redirect "/asset_management?error=Failed to upload asset: #{e.message}"
1238
+ end
1239
+ end
1240
+
1241
+ # Copy asset from global to view
1242
+ post '/asset_management/copy' do
1243
+ @current_view = @api&.current_view
1244
+ if @current_view.nil?
1245
+ redirect "/?error=No view selected. Please select a view first."
1246
+ return
1247
+ end
1248
+
1249
+ begin
1250
+ source = params[:source] # 'global' or 'library'
1251
+ filename = params[:filename]
1252
+
1253
+ if filename.nil? || filename.empty?
1254
+ redirect "/asset_management?error=No filename specified"
1255
+ return
1256
+ end
1257
+
1258
+ # Determine source file
1259
+ case source
1260
+ when 'global'
1261
+ source_file = @api.root/"assets"/filename
1262
+ when 'library'
1263
+ source_file = @api.root/"assets"/"library"/filename
1264
+ else
1265
+ redirect "/asset_management?error=Invalid source"
1266
+ return
1267
+ end
1268
+
1269
+ unless File.exist?(source_file)
1270
+ redirect "/asset_management?error=Source file not found"
1271
+ return
1272
+ end
1273
+
1274
+ # Copy to view assets
1275
+ target_dir = @api.root/"views"/@current_view.name/"assets"
1276
+ FileUtils.mkdir_p(target_dir)
1277
+ target_file = target_dir/filename
1278
+ FileUtils.cp(source_file, target_file)
1279
+
1280
+ redirect "/asset_management?message=Asset '#{filename}' copied successfully to view"
1281
+ rescue => e
1282
+ redirect "/asset_management?error=Failed to copy asset: #{e.message}"
1283
+ end
1284
+ end
1285
+
1286
+ # Delete asset
1287
+ post '/asset_management/delete' do
1288
+ @current_view = @api&.current_view
1289
+ if @current_view.nil?
1290
+ redirect "/?error=No view selected. Please select a view first."
1291
+ return
1292
+ end
1293
+
1294
+ begin
1295
+ target = params[:target] # 'global', 'library', or 'view'
1296
+ filename = params[:filename]
1297
+
1298
+ if filename.nil? || filename.empty?
1299
+ redirect "/asset_management?error=No filename specified"
1300
+ return
1301
+ end
1302
+
1303
+ # Determine target file
1304
+ case target
1305
+ when 'global'
1306
+ target_file = @api.root/"assets"/filename
1307
+ when 'library'
1308
+ target_file = @api.root/"assets"/"library"/filename
1309
+ when 'view'
1310
+ target_file = @api.root/"views"/@current_view.name/"assets"/filename
1311
+ else
1312
+ redirect "/asset_management?error=Invalid target"
1313
+ return
1314
+ end
1315
+
1316
+ unless File.exist?(target_file)
1317
+ redirect "/asset_management?error=File not found"
1318
+ return
1319
+ end
1320
+
1321
+ # Delete the file
1322
+ File.delete(target_file)
1323
+
1324
+ redirect "/asset_management?message=Asset '#{filename}' deleted successfully"
1325
+ rescue => e
1326
+ redirect "/asset_management?error=Failed to delete asset: #{e.message}"
1327
+ end
1328
+ end
1329
+
1330
+ # Helper method to update status
1331
+ private def update_config_status(view_name, config_name, status)
1332
+ status_file = @api.root/"views"/view_name/"config"/"status.txt"
1333
+ return unless File.exist?(status_file)
1334
+
1335
+ content = File.read(status_file)
1336
+ lines = content.lines.map do |line|
1337
+ if line.strip.start_with?("#{config_name} ")
1338
+ "#{config_name} #{status ? 'y' : 'n'}\n"
1339
+ else
1340
+ line
1341
+ end
1342
+ end
1343
+ File.write(status_file, lines.join)
1344
+ end
1345
+
1346
+ # Helper method for formatting file sizes
1347
+ def number_to_human_size(bytes)
1348
+ return '0 Bytes' if bytes == 0
1349
+ k = 1024
1350
+ sizes = ['Bytes', 'KB', 'MB', 'GB']
1351
+ i = (Math.log(bytes) / Math.log(k)).floor
1352
+ "#{(bytes / k**i.to_f).round(2)} #{sizes[i]}"
1353
+ end
1354
+
1355
+ def get_image_dimensions(file_path)
1356
+ return nil unless File.exist?(file_path)
1357
+
1358
+ # Check if it's an image file
1359
+ image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
1360
+ return nil unless image_extensions.any? { |ext| file_path.downcase.end_with?(ext) }
1361
+
1362
+ # Check if FastImage is available
1363
+ return nil unless defined?(FastImage)
1364
+
1365
+ begin
1366
+ dimensions = FastImage.size(file_path)
1367
+ return dimensions ? "#{dimensions[0]}×#{dimensions[1]}" : nil
1368
+ rescue => e
1369
+ # If FastImage fails, return nil
1370
+ return nil
1371
+ end
1372
+ end
1373
+ end
1374
+
1375
+ # Start the server if this file is run directly
1376
+ if __FILE__ == $0
1377
+ ScriptoriumWeb.run!
1378
+ end