scriptorium 0.6.1 → 0.7.2

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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/assets/icons/social/reddit.png +0 -0
  3. data/assets/icons/social/x-logo.png +0 -0
  4. data/assets/imagenotfound.jpg +0 -0
  5. data/bin/sblog +84 -5
  6. data/bin/scriptorium +1 -0
  7. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
  8. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
  9. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
  10. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
  11. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
  12. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
  13. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
  14. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
  15. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  16. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  17. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  18. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  19. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  20. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  21. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  22. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  23. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  24. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  25. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  26. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  27. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  28. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  29. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  30. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  31. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  32. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  33. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  34. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  35. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  36. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  37. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  38. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  39. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  40. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  41. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  42. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  43. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  44. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  45. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  46. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  47. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  48. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  49. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  50. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  51. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  52. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  53. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  54. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  55. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  56. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  57. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  58. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  59. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  60. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  61. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  62. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  63. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  64. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  65. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  66. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  67. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  68. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  69. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  70. data/doc/imported/0018-ruby-puns/post.html +46 -0
  71. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  72. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  73. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  74. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  75. data/doc/imported/0020-fffff/metadata.txt +7 -0
  76. data/doc/imported/0020-fffff/post.html +24 -0
  77. data/doc/imported/0020-fffff/source.lt3 +12 -0
  78. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  79. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  80. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  81. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  82. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  83. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  84. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  85. data/doc/imported/0024-just-a-test/post.html +24 -0
  86. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  87. data/doc/imported/import_summary.txt +98 -0
  88. data/doc/livetext-informal-spec.txt +65 -0
  89. data/doc/myuserdoc/ch-0.lt3 +31 -0
  90. data/doc/myuserdoc/ch-1.lt3 +37 -0
  91. data/doc/myuserdoc/ch-10.lt3 +22 -0
  92. data/doc/myuserdoc/ch-2.lt3 +37 -0
  93. data/doc/myuserdoc/ch-3.lt3 +19 -0
  94. data/doc/myuserdoc/ch-4.lt3 +43 -0
  95. data/doc/myuserdoc/ch-5.lt3 +22 -0
  96. data/doc/myuserdoc/ch-6.lt3 +19 -0
  97. data/doc/myuserdoc/ch-7.lt3 +16 -0
  98. data/doc/myuserdoc/ch-8.lt3 +13 -0
  99. data/doc/myuserdoc/ch-9.lt3 +19 -0
  100. data/doc/myuserdoc/tweak.rb +18 -0
  101. data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
  102. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  103. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  104. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  105. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  106. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  107. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  108. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  109. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  110. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  111. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  112. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  113. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  114. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  115. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  116. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  117. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  118. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  119. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  120. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  121. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  122. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  123. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  124. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  125. data/doc/old-repo +1 -0
  126. data/doc/reddit_integration.md +2 -2
  127. data/doc/user.lt3 +0 -3
  128. data/lib/scriptorium/api.rb +1811 -78
  129. data/lib/scriptorium/banner_svg.rb +55 -68
  130. data/lib/scriptorium/contract.rb +3 -2
  131. data/lib/scriptorium/exceptions.rb +133 -102
  132. data/lib/scriptorium/helpers.rb +282 -82
  133. data/lib/scriptorium/post.rb +81 -17
  134. data/lib/scriptorium/reddit.rb +1 -1
  135. data/lib/scriptorium/repo.rb +478 -164
  136. data/lib/scriptorium/standard_files.rb +30 -396
  137. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  138. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  139. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  140. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  141. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  142. data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
  143. data/lib/scriptorium/support/highlight/css.txt +2 -0
  144. data/lib/scriptorium/support/highlight/custom.css +119 -0
  145. data/lib/scriptorium/support/highlight/js.txt +1 -0
  146. data/lib/scriptorium/support/post_index/config.txt +15 -0
  147. data/lib/scriptorium/support/post_index/style.css +55 -0
  148. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  149. data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
  150. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  151. data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
  152. data/lib/scriptorium/theme.rb +83 -70
  153. data/lib/scriptorium/version.rb +2 -2
  154. data/lib/scriptorium/view.rb +194 -149
  155. data/lib/scriptorium.rb +24 -1
  156. data/lib/skeleton.rb +4 -1
  157. data/scriptorium.gemspec +2 -1
  158. data/test/WEB_INTEGRATION_README.md +196 -0
  159. data/test/all +40 -0
  160. data/test/banner_svg/unit.rb +267 -35
  161. data/test/config/deployment.txt +5 -0
  162. data/test/integration/integration_test.rb +7 -7
  163. data/test/integration/preview_flow_test.rb +94 -0
  164. data/test/livetext_plugin_test.rb +453 -182
  165. data/test/manual/banner-tests/test01.html +82 -18
  166. data/test/manual/banner-tests/test02.html +82 -18
  167. data/test/manual/banner-tests/test03.html +82 -18
  168. data/test/manual/banner-tests/test04.html +89 -25
  169. data/test/manual/banner-tests/test05.html +89 -25
  170. data/test/manual/banner-tests/test06.html +89 -25
  171. data/test/manual/banner-tests/test07.html +89 -25
  172. data/test/manual/banner-tests/test08.html +82 -18
  173. data/test/manual/banner-tests/test09.html +82 -18
  174. data/test/manual/banner-tests/test10.html +82 -18
  175. data/test/manual/banner-tests/test11.html +82 -18
  176. data/test/manual/banner-tests/test12.html +82 -18
  177. data/test/manual/banner-tests/test13.html +82 -18
  178. data/test/manual/banner-tests/test14.html +82 -18
  179. data/test/manual/banner-tests/test15.html +82 -18
  180. data/test/manual/banner-tests/test16.html +82 -18
  181. data/test/manual/banner-tests/test17.html +82 -18
  182. data/test/manual/banner-tests/test18.html +90 -26
  183. data/test/manual/banner-tests/test19.html +90 -26
  184. data/test/manual/banner-tests/test20.html +90 -26
  185. data/test/manual/banner-tests/test21.html +90 -26
  186. data/test/manual/banner-tests/test22.html +90 -26
  187. data/test/manual/banner-tests/test23.html +90 -26
  188. data/test/manual/banner-tests/test24.html +90 -26
  189. data/test/manual/banner-tests/test25.html +89 -25
  190. data/test/manual/banner_environment.rb +15 -2
  191. data/test/manual/codemirror_demo.html +773 -0
  192. data/test/manual/create_posts_for_web.rb +114 -0
  193. data/test/manual/preview_manual_test.rb +129 -0
  194. data/test/manual/test_banner_features.rb +14 -14
  195. data/test/manual/test_banner_integration.rb +115 -0
  196. data/test/manual/test_banner_radial.rb +87 -0
  197. data/test/manual/test_syntax_highlighting.rb +60 -40
  198. data/test/support/preview_utils.rb +88 -0
  199. data/test/test_gem_assets.rb +48 -0
  200. data/test/test_helpers.rb +10 -0
  201. data/test/tui_editor_integration_test.rb +15 -15
  202. data/test/tui_integration_test.rb +687 -441
  203. data/test/unit/api.rb +757 -37
  204. data/test/unit/asset_management.rb +195 -221
  205. data/test/unit/backup_test.rb +451 -0
  206. data/test/unit/contract_test.rb +1 -23
  207. data/test/unit/core.rb +415 -61
  208. data/test/unit/deploy_config_test.rb +248 -0
  209. data/test/unit/deploy_test.rb +312 -21
  210. data/test/unit/edit_post_test.rb +168 -0
  211. data/test/unit/gem_asset_management.rb +36 -42
  212. data/test/unit/livetext_basic.rb +23 -35
  213. data/test/unit/livetext_compatibility.rb +7 -14
  214. data/test/unit/parse_cmd_test.rb +260 -0
  215. data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
  216. data/test/unit/post.rb +91 -26
  217. data/test/unit/post_index_config_test.rb +258 -0
  218. data/test/unit/post_state_helpers_test.rb +137 -0
  219. data/test/unit/read_commented_file_test.rb +8 -6
  220. data/test/unit/repo.rb +75 -54
  221. data/test/unit/social_test.rb +41 -44
  222. data/test/unit/syntax_highlighting.rb +70 -0
  223. data/test/unit/theme_management_test.rb +91 -0
  224. data/test/unit/view.rb +79 -12
  225. data/test/unit/widgets.rb +8 -8
  226. data/test/web_integration_test.rb +231 -0
  227. data/test/web_test_helper.rb +218 -0
  228. data/test/web_workflow_test.rb +527 -0
  229. data/ui/tui/bin/scriptorium +885 -415
  230. data/ui/web/app/app.rb +1398 -176
  231. data/ui/web/app/assets/livetext_mode.js +244 -0
  232. data/ui/web/app/error_helpers.rb +16 -16
  233. data/ui/web/app/views/advanced_config.erb +8 -2
  234. data/ui/web/app/views/asset_management.erb +56 -0
  235. data/ui/web/app/views/backup_management.erb +238 -0
  236. data/ui/web/app/views/config_widget.erb +232 -0
  237. data/ui/web/app/views/dashboard.erb +64 -72
  238. data/ui/web/app/views/deploy_config.erb +3 -0
  239. data/ui/web/app/views/edit_pages.erb +170 -2
  240. data/ui/web/app/views/edit_post.erb +130 -9
  241. data/ui/web/app/views/edit_theme.erb +73 -0
  242. data/ui/web/app/views/edit_theme_file.erb +74 -0
  243. data/ui/web/app/views/theme_management.erb +130 -0
  244. data/ui/web/app/views/view_dashboard.erb +666 -25
  245. data/ui/web/app/views/widgets.erb +249 -0
  246. data/ui/web/bin/scriptorium-web +35 -24
  247. data/ui/web/tmp/timing.log +17 -0
  248. data/ui/web/tmp/web_server.log +0 -5
  249. metadata +190 -116
  250. data/assets/back-icon.png +0 -0
  251. data/assets/icons/facebook.svg +0 -1
  252. data/assets/icons/github.svg +0 -1
  253. data/assets/icons/instagram.svg +0 -1
  254. data/assets/icons/reddit.svg +0 -1
  255. data/assets/icons/x.svg +0 -1
  256. data/assets/icons/youtube.svg +0 -1
  257. data/bin/scriptorium +0 -1511
  258. data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
  259. data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
  260. data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
  261. data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
  262. data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
  263. data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
  264. data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
  265. data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
  266. data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
  267. data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
  268. data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
  269. data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
  270. data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
  271. data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
  272. data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
  273. data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
  274. data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
  275. data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
  276. data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
  277. data/lib/scriptorium/syntax_highlighter.rb +0 -234
  278. data/test/manual/deploy_symlink_demo.rb +0 -142
  279. data/test/manual/symlink_demo.rb +0 -117
  280. data/test/manual/test2.rb +0 -12
  281. data/test/manual/test_banner_from_file.rb +0 -150
  282. data/test/manual/test_banner_in_header.rb +0 -35
  283. data/test/manual/test_code_highlighting.rb +0 -68
  284. data/test/manual/test_complex_header.rb +0 -74
  285. data/test/manual/test_empty_header.rb +0 -32
  286. data/test/manual/test_radial_custom.rb +0 -58
  287. data/test/manual/test_radial_large_radius.rb +0 -52
  288. data/test/manual/test_svg_debug.rb +0 -47
  289. data/test/pages-demo/config/currentview.txt +0 -1
  290. data/test/pages-demo/views/demo/config/common.js +0 -57
  291. data/test/pages-demo/views/demo/config/footer.txt +0 -1
  292. data/test/pages-demo/views/demo/config/global-head.txt +0 -8
  293. data/test/pages-demo/views/demo/config/header.txt +0 -1
  294. data/test/pages-demo/views/demo/config/layout.txt +0 -1
  295. data/test/pages-demo/views/demo/config/left.txt +0 -1
  296. data/test/pages-demo/views/demo/config/main.txt +0 -1
  297. data/test/pages-demo/views/demo/config/right.txt +0 -1
  298. data/test/pages-demo/views/demo/config.txt +0 -3
  299. data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
  300. data/test/pages-demo/views/demo/output/panes/header.html +0 -1
  301. data/test/pages-demo/views/demo/output/panes/left.html +0 -1
  302. data/test/pages-demo/views/demo/output/panes/main.html +0 -1
  303. data/test/pages-demo/views/demo/output/panes/right.html +0 -1
  304. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
  305. data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
  306. data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
  307. data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
  308. data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
  309. data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
  310. data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
  311. data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
  312. data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
  313. data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
  314. data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
  315. data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
  316. data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
  317. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
  318. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
  319. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
  320. data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
  321. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
  322. data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
  323. data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
  324. data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
  325. data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
  326. data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
  327. data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
  328. data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
  329. data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
  330. data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
  331. data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
  332. data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
  333. data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
  334. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
  335. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
  336. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
  337. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
  338. data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
  339. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
  340. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
  341. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
  342. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
  343. data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
  344. data/ui/web/tmp/web_server.pid +0 -1
  345. /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
  346. /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
  347. /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
  348. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
  349. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
  350. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
  351. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
  352. /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
  353. /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
  354. /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
  355. /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
  356. /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
  357. /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
  358. /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/ui/web/app/app.rb CHANGED
@@ -1,5 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'optparse'
4
+
5
+ # Parse command line arguments for test mode BEFORE requiring Sinatra
6
+ # Starting web app, ARGV: #{ARGV.inspect}
7
+ TEST_MODE = false
8
+ OptionParser.new do |opts|
9
+ opts.on("--test", "Use test repository (scriptorium-TEST)") do
10
+ TEST_MODE = true
11
+ # --test flag detected
12
+ end
13
+ end.parse!
14
+
15
+ # Command line parsing complete, test_mode: #{TEST_MODE}
16
+ # ARGV remaining: #{ARGV.inspect}
17
+
3
18
  require 'sinatra'
4
19
  require 'sinatra/reloader' if development?
5
20
  require 'fileutils'
@@ -12,14 +27,59 @@ end
12
27
  require_relative '../../../lib/scriptorium'
13
28
  require_relative 'error_helpers'
14
29
 
15
- include ErrorHelpers
16
-
17
30
  class ScriptoriumWeb < Sinatra::Base
31
+ include ErrorHelpers
32
+ include Scriptorium::Helpers
33
+
18
34
  set :port, 4567
19
35
  set :bind, '0.0.0.0'
20
36
  set :views, File.join(__dir__, 'views')
21
37
  set :show_exceptions, false # Disable Sinatra's default error display
22
38
 
39
+ # Configure static file serving for assets
40
+ configure do
41
+ # Set public folder to serve static files from the current view's output directory
42
+ # This will be updated dynamically based on the current view
43
+ set :public_folder, File.join(__dir__, '..', 'scriptorium-TEST', 'views', 'computing', 'output')
44
+ end
45
+
46
+ # Update static file serving based on current view
47
+ before do
48
+ if @api&.current_view
49
+ public_path = @api.root/"views"/@api.current_view.name/"output"
50
+ if Dir.exist?(public_path)
51
+ settings.public_folder = public_path.to_s
52
+ end
53
+ end
54
+ end
55
+
56
+ # Helper method to render dashboard with error/message
57
+ def render_dashboard(error: nil, message: nil)
58
+ @error = error
59
+ @message = message
60
+ @current_view = @api&.current_view
61
+ @views = @api&.views || []
62
+ @posts = []
63
+ erb :dashboard
64
+ end
65
+
66
+ # Helper method to add file/line info to error messages
67
+ def error_with_location(error, message)
68
+ error_location = "#{error.backtrace.first}" if error.backtrace
69
+ result = message
70
+ result += " (#{error_location})" if error_location
71
+ result
72
+ end
73
+
74
+ # Set test mode
75
+ def self.test_mode=(value)
76
+ @@test_mode = value
77
+ end
78
+
79
+ def self.test_mode
80
+ @@test_mode
81
+ end
82
+
23
83
  # Enable reloading in development
24
84
  configure :development do
25
85
  register Sinatra::Reloader
@@ -36,11 +96,24 @@ class ScriptoriumWeb < Sinatra::Base
36
96
  # Initialize API instance
37
97
  before do
38
98
  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)
99
+ # Use the TEST_MODE constant that was set before OptionParser consumed ARGV
100
+ # Before block - test_mode: #{TEST_MODE}
101
+ @api = Scriptorium::API.new(testmode: TEST_MODE)
102
+
103
+ if TEST_MODE
104
+ # Use test repository in the ui/web/ directory
105
+ test_repo_path = File.join(__dir__, "..", "scriptorium-TEST")
106
+ # Opening test repo: #{test_repo_path}
107
+ @api.open_repo(test_repo_path) if Dir.exist?(test_repo_path)
108
+ else
109
+ # Use production repository
110
+ home = ENV['HOME']
111
+ production_path = "#{home}/.scriptorium"
112
+ # Opening production repo: #{production_path}
113
+ @api.open_repo(production_path) if Dir.exist?(production_path)
114
+ end
43
115
  rescue => e
116
+ # Error in before block: #{e.message}
44
117
  @api = nil
45
118
  end
46
119
  end
@@ -54,10 +127,13 @@ class ScriptoriumWeb < Sinatra::Base
54
127
 
55
128
  # Only try to load posts if we have a current view
56
129
  if @current_view
57
- @posts = @api.posts(@current_view.name) || []
130
+ File.write("/tmp/debug.log", "DEBUG: Route reached, current_view: #{@current_view.name}\n", mode: 'a')
131
+ @posts = @api.posts(@current_view.name, include_deleted: true) || []
132
+ File.write("/tmp/debug.log", "DEBUG: Posts loaded: #{@posts.length}\n", mode: 'a')
58
133
  if @posts.length > 0
59
134
  end
60
135
  else
136
+ File.write("/tmp/debug.log", "DEBUG: No current view\n", mode: 'a')
61
137
  @posts = []
62
138
  end
63
139
  else
@@ -77,16 +153,16 @@ class ScriptoriumWeb < Sinatra::Base
77
153
  view_name = params[:view_name]
78
154
 
79
155
  if view_name.nil? || view_name.strip.empty?
80
- redirect "/?error=No view selected"
156
+ render_dashboard(error: "No view selected")
81
157
  return
82
158
  end
83
159
 
84
160
  begin
85
161
  view = @api.lookup_view(view_name)
86
162
  @api.view(view_name)
87
- redirect '/?message=View changed successfully'
163
+ render_dashboard(message: "View changed successfully")
88
164
  rescue => e
89
- redirect "/?error=Failed to change view: #{e.message}"
165
+ render_dashboard(error: error_with_location(e, "Failed to change view: #{e.message}"))
90
166
  end
91
167
  end
92
168
 
@@ -130,29 +206,32 @@ class ScriptoriumWeb < Sinatra::Base
130
206
  return
131
207
  end
132
208
 
209
+ # Get selected views from checkboxes
210
+ selected_views = params[:views] || [current_view.name]
211
+ selected_views = [current_view.name] if selected_views.empty?
212
+
213
+ # Process tags
214
+ tags = params[:tags]&.strip
215
+ tags = tags&.split(',')&.map(&:strip) if tags && !tags.empty?
216
+
133
217
  # Create a draft first
134
218
  draft_path = @api.create_draft(
135
219
  title: params[:title].strip,
136
220
  body: "", # Empty body to start
137
- views: current_view.name,
138
- tags: nil,
139
- blurb: nil
221
+ views: selected_views,
222
+ tags: tags,
223
+ blurb: params[:blurb]&.strip
140
224
  )
141
225
 
142
226
  # Convert draft to post immediately
143
227
  post_num = @api.finish_draft(draft_path)
144
228
  # Generate the post to create meta.txt and other files
145
229
  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
230
  @api.generate_post(post_num)
150
- STDERR.puts "DEBUG: generate_post completed successfully"
151
231
  # Check if meta.txt was created
152
232
  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})"
233
+ # Redirect back to dashboard with modal parameter to open CodeMirror editor
234
+ redirect "/view/#{current_view.name}?edit_post=#{post_num}"
156
235
  rescue => e
157
236
  # Log the actual error for debugging
158
237
  STDERR.puts "ERROR in generate_post: #{e.class}: #{e.message}"
@@ -182,14 +261,46 @@ class ScriptoriumWeb < Sinatra::Base
182
261
  return
183
262
  end
184
263
 
185
- # Redirect to the edit page
186
- redirect "/edit_post/#{params[:post_id]}"
264
+ # Redirect back to the view dashboard
265
+ current_view = @api&.current_view
266
+ if current_view
267
+ redirect "/view/#{current_view.name}?message=Post saved successfully"
268
+ else
269
+ redirect "/?message=Post saved successfully"
270
+ end
187
271
  rescue => e
188
272
  error_info = friendly_error_message(e)
189
273
  redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
190
274
  end
191
275
  end
192
276
 
277
+ # API endpoint to get post content for modal
278
+ get '/api/post_content/:id' do
279
+ begin
280
+ post_id = params[:id].to_i
281
+ post = @api.post(post_id)
282
+
283
+ if post.nil?
284
+ status 404
285
+ return "Post not found"
286
+ end
287
+
288
+ # Read the source file
289
+ source_file = @api.root/"posts"/"#{post.num.to_s.rjust(4, '0')}"/"source.lt3"
290
+ if File.exist?(source_file)
291
+ content = File.read(source_file)
292
+ content_type :text
293
+ content
294
+ else
295
+ status 404
296
+ "Source file not found"
297
+ end
298
+ rescue => e
299
+ status 500
300
+ "Error loading post content: #{e.message}"
301
+ end
302
+ end
303
+
193
304
  # Show edit post page
194
305
  get '/edit_post/:id' do
195
306
  post_id = params[:id]&.to_i
@@ -209,11 +320,14 @@ class ScriptoriumWeb < Sinatra::Base
209
320
  # Read the source file content
210
321
  source_file = @api.root/"posts"/@post.num/"source.lt3"
211
322
  if File.exist?(source_file)
212
- @content = File.read(source_file)
323
+ @content = read_file(source_file)
213
324
  else
214
325
  @content = "# #{@post.title}\n\n"
215
326
  end
216
327
 
328
+ # Set current view for template
329
+ @current_view = @api&.current_view
330
+
217
331
  erb :edit_post
218
332
  rescue => e
219
333
  redirect "/?error=Failed to load post: #{e.message}"
@@ -222,38 +336,84 @@ class ScriptoriumWeb < Sinatra::Base
222
336
 
223
337
  # Save edited post
224
338
  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
339
  begin
340
+ File.write('/tmp/save_post_debug.log', "=== SAVE POST ATTEMPT ===\n", mode: 'a')
341
+ File.write('/tmp/save_post_debug.log', "Time: #{Time.now}\n", mode: 'a')
342
+ File.write('/tmp/save_post_debug.log', "Post ID: #{params[:id]}\n", mode: 'a')
343
+ File.write('/tmp/save_post_debug.log', "Content length: #{params[:content]&.length || 0}\n", mode: 'a')
344
+
345
+ File.write('/tmp/save_post_debug.log', "API instance: #{@api.inspect}\n", mode: 'a')
346
+
347
+ post_id = params[:id]&.to_i
348
+ content = params[:content]
349
+
350
+ if post_id.nil? || post_id <= 0
351
+ File.write('/tmp/save_post_debug.log', "ERROR: Invalid post ID\n", mode: 'a')
352
+ redirect "/?error=Invalid post ID"
353
+ return
354
+ end
355
+
356
+ if content.nil?
357
+ File.write('/tmp/save_post_debug.log', "ERROR: No content provided\n", mode: 'a')
358
+ redirect "/edit_post/#{post_id}?error=No content provided"
359
+ return
360
+ end
361
+
362
+ File.write('/tmp/save_post_debug.log', "Looking up post #{post_id}\n", mode: 'a')
239
363
  post = @api.post(post_id)
240
364
  if post.nil?
365
+ File.write('/tmp/save_post_debug.log', "ERROR: Post not found\n", mode: 'a')
241
366
  redirect "/?error=Post not found"
242
367
  return
243
368
  end
244
369
 
370
+ File.write('/tmp/save_post_debug.log', "Post found: #{post.inspect}\n", mode: 'a')
371
+ File.write('/tmp/save_post_debug.log', "Post num: #{post.num}\n", mode: 'a')
372
+
245
373
  # Write the content to the source file
246
374
  source_file = @api.root/"posts"/post.num/"source.lt3"
247
- File.write(source_file, content)
375
+ File.write('/tmp/save_post_debug.log', "Source file: #{source_file}\n", mode: 'a')
376
+ write_file(source_file, content)
377
+ File.write('/tmp/save_post_debug.log', "File written successfully\n", mode: 'a')
248
378
 
249
379
  # Generate the post after saving
250
- @api.generate_post(post_id)
380
+ File.write('/tmp/save_post_debug.log', "Generating post...\n", mode: 'a')
381
+ begin
382
+ @api.generate_post(post_id)
383
+ File.write('/tmp/save_post_debug.log', "Post generated successfully\n", mode: 'a')
384
+ rescue => e
385
+ File.write('/tmp/save_post_debug.log', "Generate post failed: #{e.class}: #{e.message}\n", mode: 'a')
386
+ File.write('/tmp/save_post_debug.log', "Backtrace: #{e.backtrace.first(3).join("\n")}\n", mode: 'a')
387
+ raise e
388
+ end
251
389
 
252
- redirect "/?message=Post ##{post_id} saved and generated successfully"
390
+ # Regenerate the view index to include the updated post
391
+ File.write('/tmp/save_post_debug.log', "Regenerating view index...\n", mode: 'a')
392
+ begin
393
+ current_view = @api&.current_view
394
+ if current_view
395
+ @api.generate_view(current_view.name)
396
+ File.write('/tmp/save_post_debug.log', "View index regenerated successfully\n", mode: 'a')
397
+ end
398
+ rescue => e
399
+ File.write('/tmp/save_post_debug.log', "View regeneration failed: #{e.class}: #{e.message}\n", mode: 'a')
400
+ # Don't fail the save if view regeneration fails
401
+ end
402
+
403
+ File.write('/tmp/save_post_debug.log', "SUCCESS: Redirecting to view dashboard\n", mode: 'a')
404
+ current_view = @api&.current_view
405
+ if current_view
406
+ redirect "/view/#{current_view.name}?message=Post saved successfully"
407
+ else
408
+ redirect "/?message=Post ##{post_id} saved and generated successfully"
409
+ end
253
410
  rescue => e
254
- redirect "/edit_post/#{post_id}?error=Failed to save post: #{e.message}"
255
- end
411
+ File.write('/tmp/save_post_debug.log', "EXCEPTION: #{e.class}: #{e.message}\n", mode: 'a')
412
+ File.write('/tmp/save_post_debug.log', "Backtrace: #{e.backtrace.first(5).join("\n")}\n", mode: 'a')
413
+ error_location = e.backtrace&.first || "unknown location"
414
+ redirect "/edit_post/#{post_id}?error=Failed to save post: #{e.message} at #{error_location}"
256
415
  end
416
+ end
257
417
 
258
418
  # Generate post
259
419
  post '/generate_post' do
@@ -285,44 +445,86 @@ class ScriptoriumWeb < Sinatra::Base
285
445
 
286
446
  begin
287
447
  if view_name.nil? || view_name.strip.empty?
288
- redirect "/?error=No view specified"
448
+ render_dashboard(error: "No view specified")
289
449
  return
290
450
  end
291
451
 
292
452
  # Generate the view
293
453
  @api.generate_view(view_name)
294
- redirect "/?message=View '#{view_name}' generated successfully"
454
+ render_dashboard(message: "View '#{view_name}' generated successfully")
295
455
  rescue => e
296
- redirect "/?error=Failed to generate view: #{e.message}"
456
+ render_dashboard(error: error_with_location(e, "Failed to generate view: #{e.message}"))
297
457
  end
298
458
  end
299
459
 
300
460
  # Preview view
301
- post '/preview_view' do
461
+ get '/preview' do
462
+ @current_view = @api&.current_view
463
+ if @current_view.nil?
464
+ render_dashboard(error: "No view selected. Please select a view first.")
465
+ return
466
+ end
467
+
468
+ begin
469
+ # Generate the view first to ensure it's up to date
470
+ @api.generate_view(@current_view.name)
471
+
472
+ # Redirect to the index route under /preview/:view_name so relative links resolve
473
+ redirect "/preview/#{@current_view.name}/index.html"
474
+ rescue => e
475
+ render_dashboard(error: error_with_location(e, "Failed to preview view: #{e.message}"))
476
+ end
477
+ end
478
+
479
+ # Preview specific view index
480
+ get '/preview/:view_name/index.html' do
302
481
  view_name = params[:view_name]
303
482
 
304
483
  begin
305
484
  if view_name.nil? || view_name.strip.empty?
306
- redirect "/?error=No view specified"
307
- return
485
+ status 400
486
+ return "Bad request: missing view name"
308
487
  end
309
488
 
310
- # Generate the view first to ensure it's up to date
489
+ # Generate the view to ensure it's up to date
311
490
  @api.generate_view(view_name)
312
491
 
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
-
492
+ # Serve the generated index.html file
493
+ index_file = @api.root/"views"/view_name/"output"/"index.html"
317
494
  if File.exist?(index_file)
318
- # Return the HTML content directly for preview
319
495
  content_type :html
320
- File.read(index_file)
496
+ read_file(index_file)
497
+ else
498
+ status 404
499
+ "Index file not found for view: #{view_name}"
500
+ end
501
+ rescue => e
502
+ status 500
503
+ "Error loading view: #{e.message}"
504
+ end
505
+ end
506
+
507
+ # Serve post_index.html fragment for SPA back navigation
508
+ get '/preview/:view_name/post_index.html' do
509
+ view_name = params[:view_name]
510
+ begin
511
+ if view_name.nil? || view_name.strip.empty?
512
+ status 400
513
+ return "Bad request: missing view name"
514
+ end
515
+ # Ensure view is generated
516
+ @api.generate_view(view_name)
517
+ fragment = @api.root/"views"/view_name/"output"/"post_index.html"
518
+ if File.exist?(fragment)
519
+ content_type :html
520
+ read_file(fragment)
321
521
  else
322
- redirect "/?error=Preview file not found - view may not have been generated properly"
522
+ status 404
523
+ "Not found"
323
524
  end
324
525
  rescue => e
325
- redirect "/?error=Failed to preview view: #{e.message}"
526
+ status 500
527
+ "Error loading post_index: #{e.message}"
326
528
  end
327
529
  end
328
530
 
@@ -331,35 +533,267 @@ class ScriptoriumWeb < Sinatra::Base
331
533
  view_name = params[:view_name]
332
534
  filename = params[:filename]
333
535
 
334
- STDERR.puts "DEBUG: Preview request - view_name: #{view_name}, filename: #{filename}"
536
+ begin
537
+ if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
538
+ status 404
539
+ return "File not found"
540
+ end
541
+
542
+ # Check if view has been generated (index.html exists)
543
+ index_file = @api.root/"views"/view_name/"output"/"index.html"
544
+ unless File.exist?(index_file)
545
+ status 404
546
+ return "View '#{view_name}' has not been generated. Please generate the view first."
547
+ end
548
+
549
+ # Construct the file path
550
+ post_file = @api.root/"views"/view_name/"output"/"posts"/filename
551
+
552
+ if File.exist?(post_file)
553
+ content_type :html
554
+ read_file(post_file)
555
+ else
556
+ status 404
557
+ "File not found: #{filename}"
558
+ end
559
+ rescue => e
560
+ status 500
561
+ "Error loading file: #{e.message}"
562
+ end
563
+ end
564
+
565
+ # Serve post content for iframe (with syntax highlighting)
566
+ get '/preview/:view_name/posts/:filename/content' do
567
+ view_name = params[:view_name]
568
+ filename = params[:filename]
335
569
 
336
570
  begin
337
571
  if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
338
- STDERR.puts "DEBUG: Missing parameters"
339
572
  status 404
340
573
  return "File not found"
341
574
  end
342
575
 
343
576
  # Construct the file path
344
577
  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
578
 
348
579
  if File.exist?(post_file)
349
580
  content_type :html
350
- File.read(post_file)
581
+
582
+ # Read the post content
583
+ post_content = read_file(post_file)
584
+
585
+ # Wrap in HTML document with syntax highlighting (Highlight.js)
586
+ html = <<~HTML
587
+ <!DOCTYPE html>
588
+ <html>
589
+ <head>
590
+ <meta charset="utf-8">
591
+ <title>Post Content</title>
592
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
593
+ <style>
594
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; line-height: 1.6; }
595
+ pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
596
+ code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }
597
+ </style>
598
+ </head>
599
+ <body>
600
+ #{post_content}
601
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
602
+ <script>
603
+ document.addEventListener('DOMContentLoaded', function() {
604
+ if (typeof hljs !== 'undefined') { hljs.highlightAll(); }
605
+ });
606
+ </script>
607
+ </body>
608
+ </html>
609
+ HTML
610
+
611
+ html
612
+ else
613
+ status 404
614
+ "File not found: #{filename}"
615
+ end
616
+ rescue => e
617
+ status 500
618
+ "Error loading file: #{e.message}"
619
+ end
620
+ end
621
+
622
+ # Serve permalink files for preview
623
+ get '/preview/:view_name/permalink/:filename' do
624
+ view_name = params[:view_name]
625
+ filename = params[:filename]
626
+
627
+ begin
628
+ if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
629
+ status 404
630
+ return "File not found"
631
+ end
632
+
633
+ # Check if view has been generated (index.html exists)
634
+ index_file = @api.root/"views"/view_name/"output"/"index.html"
635
+ unless File.exist?(index_file)
636
+ status 404
637
+ return "View '#{view_name}' has not been generated. Please generate the view first."
638
+ end
639
+
640
+ # Construct the file path
641
+ permalink_file = @api.root/"views"/view_name/"output"/"permalink"/filename
642
+
643
+ if File.exist?(permalink_file)
644
+ content_type :html
645
+ read_file(permalink_file)
646
+ else
647
+ status 404
648
+ "File not found: #{filename}"
649
+ end
650
+ rescue => e
651
+ status 500
652
+ "Error loading file: #{e.message}"
653
+ end
654
+ end
655
+
656
+ # Serve assets for preview
657
+ get '/preview/:view_name/assets/*' do
658
+ view_name = params[:view_name]
659
+ asset_path = params[:splat].first
660
+
661
+ begin
662
+ if view_name.nil? || view_name.strip.empty? || asset_path.nil? || asset_path.strip.empty?
663
+ status 404
664
+ return "Asset not found"
665
+ end
666
+
667
+ # Check if view has been generated (index.html exists)
668
+ index_file = @api.root/"views"/view_name/"output"/"index.html"
669
+ unless File.exist?(index_file)
670
+ status 404
671
+ return "View '#{view_name}' has not been generated. Please generate the view first."
672
+ end
673
+
674
+ # Construct the asset file path (serve from generated output assets)
675
+ asset_file = @api.root/"views"/view_name/"output"/"assets"/asset_path
676
+ # Fallback: if not present in output assets, try view assets directly
677
+ unless File.exist?(asset_file)
678
+ fallback_asset = @api.root/"views"/view_name/"assets"/asset_path
679
+ asset_file = fallback_asset if File.exist?(fallback_asset)
680
+ end
681
+
682
+ if File.exist?(asset_file)
683
+ # Set appropriate content type based on file extension
684
+ case File.extname(asset_file).downcase
685
+ when '.png'
686
+ content_type 'image/png'
687
+ when '.jpg', '.jpeg'
688
+ content_type 'image/jpeg'
689
+ when '.gif'
690
+ content_type 'image/gif'
691
+ when '.svg'
692
+ content_type 'image/svg+xml'
693
+ when '.css'
694
+ content_type 'text/css'
695
+ when '.js'
696
+ content_type 'application/javascript'
697
+ else
698
+ content_type 'application/octet-stream'
699
+ end
700
+
701
+ read_file(asset_file)
702
+ else
703
+ status 404
704
+ "Asset not found: #{asset_path}"
705
+ end
706
+ rescue => e
707
+ status 500
708
+ "Error loading asset: #{e.message}"
709
+ end
710
+ end
711
+
712
+ # (timing route removed)
713
+
714
+ # Serve post files relative to preview route
715
+ get '/posts/:filename' do
716
+ filename = params[:filename]
717
+ @current_view = @api&.current_view
718
+
719
+ begin
720
+ if filename.nil? || filename.strip.empty? || @current_view.nil?
721
+ status 404
722
+ return "File not found"
723
+ end
724
+
725
+ # Construct the file path
726
+ post_file = @api.root/"views"/@current_view.name/"output"/"posts"/filename
727
+
728
+ if File.exist?(post_file)
729
+ content_type :html
730
+ read_file(post_file)
351
731
  else
352
- STDERR.puts "DEBUG: File not found"
353
732
  status 404
354
733
  "File not found: #{filename}"
355
734
  end
356
735
  rescue => e
357
- STDERR.puts "DEBUG: Error: #{e.message}"
358
736
  status 500
359
737
  "Error loading file: #{e.message}"
360
738
  end
361
739
  end
362
740
 
741
+ # Handle direct access to posts via index.html?post=filename
742
+ get '/index.html' do
743
+ post_param = params[:post]
744
+ @current_view = @api&.current_view
745
+
746
+ begin
747
+ if @current_view.nil?
748
+ status 404
749
+ return "View not found"
750
+ end
751
+
752
+ # Always return the full index.html page
753
+ # The JavaScript will handle loading the specific post if post_param is provided
754
+ index_file = @api.root/"views"/@current_view.name/"output"/"index.html"
755
+
756
+ if File.exist?(index_file)
757
+ content_type :html
758
+ read_file(index_file)
759
+ else
760
+ status 404
761
+ "Index page not found"
762
+ end
763
+ rescue => e
764
+ status 500
765
+ "Error loading page: #{e.message}"
766
+ end
767
+ end
768
+
769
+ # Handle permalink access to posts
770
+ get '/permalink/:filename' do
771
+ filename = params[:filename]
772
+ @current_view = @api&.current_view
773
+
774
+ begin
775
+ if filename.nil? || filename.strip.empty? || @current_view.nil?
776
+ status 404
777
+ return "Post not found"
778
+ end
779
+
780
+ # Construct the file path
781
+ post_file = @api.root/"views"/@current_view.name/"output"/"posts"/filename
782
+
783
+ if File.exist?(post_file)
784
+ # Redirect to the index page with the post parameter
785
+ # This allows the JavaScript to handle the post loading properly
786
+ redirect "/index.html?post=#{filename}"
787
+ else
788
+ status 404
789
+ "Post not found: #{filename}"
790
+ end
791
+ rescue => e
792
+ status 500
793
+ "Error loading post: #{e.message}"
794
+ end
795
+ end
796
+
363
797
  # Show view configuration page
364
798
  get '/configure_view/:name' do
365
799
  begin
@@ -411,7 +845,7 @@ class ScriptoriumWeb < Sinatra::Base
411
845
  config_content += "theme #{params[:view_theme]}\n"
412
846
 
413
847
  config_file = @api.root/"views"/view_name/"config.txt"
414
- File.write(config_file, config_content)
848
+ write_file(config_file, config_content)
415
849
  end
416
850
 
417
851
  # Step 2: Save layout configuration
@@ -438,7 +872,7 @@ class ScriptoriumWeb < Sinatra::Base
438
872
 
439
873
  layout_file = @api.root/"views"/view_name/"config"/"layout.txt"
440
874
  FileUtils.mkdir_p(File.dirname(layout_file))
441
- File.write(layout_file, layout_content)
875
+ write_file(layout_file, layout_content)
442
876
  end
443
877
 
444
878
  # Step 3: Save container content files
@@ -449,7 +883,7 @@ class ScriptoriumWeb < Sinatra::Base
449
883
  if params[content_param]
450
884
  content_file = @api.root/"views"/view_name/"config"/"#{container}.txt"
451
885
  FileUtils.mkdir_p(File.dirname(content_file))
452
- File.write(content_file, params[content_param])
886
+ write_file(content_file, params[content_param])
453
887
 
454
888
  # If this is header with "banner svg", create default svg.txt
455
889
  if container == 'header' && params[content_param].strip == 'banner svg'
@@ -461,7 +895,7 @@ class ScriptoriumWeb < Sinatra::Base
461
895
  default_svg_content += "back.linear #f8f9fa #e9ecef lr\n"
462
896
  default_svg_content += "text.color #374151\n"
463
897
  default_svg_content += "title.style bold\n"
464
- File.write(svg_file, default_svg_content)
898
+ write_file(svg_file, default_svg_content)
465
899
  end
466
900
  end
467
901
  end
@@ -483,7 +917,7 @@ class ScriptoriumWeb < Sinatra::Base
483
917
 
484
918
  # Get current SVG config
485
919
  svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
486
- @svg_config = File.exist?(svg_file) ? File.read(svg_file) : ""
920
+ @svg_config = File.exist?(svg_file) ? read_file(svg_file) : ""
487
921
 
488
922
  # Generate current banner for display
489
923
  begin
@@ -498,7 +932,7 @@ class ScriptoriumWeb < Sinatra::Base
498
932
  banner.parse_header_svg
499
933
  end
500
934
 
501
- @banner_svg = banner.generate_svg
935
+ @banner_svg = banner.get_svg
502
936
  rescue => e
503
937
  @banner_svg = "<p>Error generating banner: #{e.message}</p>"
504
938
  end
@@ -520,7 +954,7 @@ class ScriptoriumWeb < Sinatra::Base
520
954
  # Save the SVG configuration
521
955
  svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
522
956
  FileUtils.mkdir_p(File.dirname(svg_file))
523
- File.write(svg_file, svg_config)
957
+ write_file(svg_file, svg_config)
524
958
 
525
959
  # Update status
526
960
  update_config_status(@current_view.name, "banner", true)
@@ -541,7 +975,7 @@ class ScriptoriumWeb < Sinatra::Base
541
975
 
542
976
  # Get current navbar config
543
977
  navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
544
- @navbar_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
978
+ @navbar_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
545
979
 
546
980
  # Generate current navbar preview
547
981
  begin
@@ -574,7 +1008,7 @@ class ScriptoriumWeb < Sinatra::Base
574
1008
 
575
1009
  # Read current navbar config
576
1010
  navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
577
- current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
1011
+ current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
578
1012
 
579
1013
  # Add new item based on action
580
1014
  if action == "link"
@@ -594,7 +1028,7 @@ class ScriptoriumWeb < Sinatra::Base
594
1028
 
595
1029
  # Save the updated configuration
596
1030
  FileUtils.mkdir_p(File.dirname(navbar_file))
597
- File.write(navbar_file, updated_config.rstrip + "\n")
1031
+ write_file(navbar_file, updated_config.rstrip + "\n")
598
1032
 
599
1033
  redirect "/navbar_config?message=#{message}"
600
1034
  rescue => e
@@ -632,7 +1066,7 @@ class ScriptoriumWeb < Sinatra::Base
632
1066
 
633
1067
  # Read current navbar config
634
1068
  navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
635
- current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
1069
+ current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
636
1070
 
637
1071
  # Find the parent and add child after it
638
1072
  lines = current_config.lines
@@ -655,7 +1089,7 @@ class ScriptoriumWeb < Sinatra::Base
655
1089
 
656
1090
  # Save the updated configuration
657
1091
  FileUtils.mkdir_p(File.dirname(navbar_file))
658
- File.write(navbar_file, new_lines.join.rstrip + "\n")
1092
+ write_file(navbar_file, new_lines.join.rstrip + "\n")
659
1093
 
660
1094
  redirect "/navbar_config?message=Added #{label} as child of #{parent}"
661
1095
  rescue => e
@@ -681,7 +1115,7 @@ class ScriptoriumWeb < Sinatra::Base
681
1115
  # Save the configuration
682
1116
  navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
683
1117
  FileUtils.mkdir_p(File.dirname(navbar_file))
684
- File.write(navbar_file, config.rstrip + "\n")
1118
+ write_file(navbar_file, config.rstrip + "\n")
685
1119
 
686
1120
  # Check for missing pages and create them
687
1121
  pages_created = []
@@ -709,7 +1143,7 @@ class ScriptoriumWeb < Sinatra::Base
709
1143
  page_file = pages_dir/filename
710
1144
  unless File.exist?(page_file)
711
1145
  content = ".page_title #{title}\n\n"
712
- File.write(page_file, content)
1146
+ write_file(page_file, content)
713
1147
  pages_created << filename
714
1148
  end
715
1149
  end
@@ -730,7 +1164,7 @@ class ScriptoriumWeb < Sinatra::Base
730
1164
  page_file = pages_dir/filename
731
1165
  unless File.exist?(page_file)
732
1166
  content = ".page_title #{title}\n\n"
733
- File.write(page_file, content)
1167
+ write_file(page_file, content)
734
1168
  pages_created << filename
735
1169
  end
736
1170
  end
@@ -766,7 +1200,7 @@ class ScriptoriumWeb < Sinatra::Base
766
1200
  Dir.glob(pages_dir/"*").each do |file|
767
1201
  next unless File.file?(file)
768
1202
  filename = File.basename(file)
769
- content = File.read(file)
1203
+ content = read_file(file)
770
1204
 
771
1205
  # Extract page title from .page_title directive
772
1206
  title = nil
@@ -791,8 +1225,15 @@ class ScriptoriumWeb < Sinatra::Base
791
1225
 
792
1226
  # Save page content
793
1227
  post '/edit_pages/save' do
1228
+ File.write('/tmp/edit_pages_debug.log', "=== SAVE ATTEMPT ===\n", mode: 'a')
1229
+ File.write('/tmp/edit_pages_debug.log', "Time: #{Time.now}\n", mode: 'a')
1230
+ File.write('/tmp/edit_pages_debug.log', "API instance: #{@api.inspect}\n", mode: 'a')
1231
+ File.write('/tmp/edit_pages_debug.log', "Current view: #{@api&.current_view&.inspect}\n", mode: 'a')
1232
+ File.write('/tmp/edit_pages_debug.log', "Params: #{params.inspect}\n", mode: 'a')
1233
+
794
1234
  @current_view = @api&.current_view
795
1235
  if @current_view.nil?
1236
+ File.write('/tmp/edit_pages_debug.log', "ERROR: No current view\n", mode: 'a')
796
1237
  redirect "/?error=No view selected. Please select a view first."
797
1238
  return
798
1239
  end
@@ -801,19 +1242,28 @@ class ScriptoriumWeb < Sinatra::Base
801
1242
  filename = params[:filename]&.strip
802
1243
  content = params[:content]&.strip || ""
803
1244
 
1245
+ File.write('/tmp/edit_pages_debug.log', "Filename: #{filename.inspect}\n", mode: 'a')
1246
+ File.write('/tmp/edit_pages_debug.log', "Content length: #{content.length}\n", mode: 'a')
1247
+
804
1248
  if filename.nil? || filename.empty?
1249
+ File.write('/tmp/edit_pages_debug.log', "ERROR: Filename is empty\n", mode: 'a')
805
1250
  redirect "/edit_pages?error=Filename is required"
806
1251
  return
807
1252
  end
808
1253
 
809
1254
  # Save the page
810
1255
  pages_dir = @api.root/"views"/@current_view.name/"pages"
1256
+ File.write('/tmp/edit_pages_debug.log', "Pages dir: #{pages_dir}\n", mode: 'a')
811
1257
  FileUtils.mkdir_p(pages_dir)
812
1258
  page_file = pages_dir/filename
1259
+ File.write('/tmp/edit_pages_debug.log', "Page file: #{page_file}\n", mode: 'a')
813
1260
  File.write(page_file, content)
1261
+ File.write('/tmp/edit_pages_debug.log', "SUCCESS: File written\n", mode: 'a')
814
1262
 
815
1263
  redirect "/edit_pages?message=Page '#{filename}' saved successfully"
816
1264
  rescue => e
1265
+ File.write('/tmp/edit_pages_debug.log', "EXCEPTION: #{e.class}: #{e.message}\n", mode: 'a')
1266
+ File.write('/tmp/edit_pages_debug.log', "Backtrace: #{e.backtrace.first(5).join("\n")}\n", mode: 'a')
817
1267
  redirect "/edit_pages?error=Failed to save page: #{e.message}"
818
1268
  end
819
1269
  end
@@ -822,68 +1272,111 @@ class ScriptoriumWeb < Sinatra::Base
822
1272
  get '/view/:name' do
823
1273
  view_name = params[:name]
824
1274
 
1275
+ # Debug logging
1276
+ File.write('/tmp/dashboard_debug.log', "Dashboard accessed for view: #{view_name} at #{Time.now}\n", mode: 'a')
1277
+
825
1278
  begin
826
1279
  # Look up the view
827
- view = @api.lookup_view(view_name)
828
- if view.nil?
1280
+ @current_view = @api.lookup_view(view_name)
1281
+ if @current_view.nil?
829
1282
  redirect "/?error=View '#{view_name}' not found"
830
1283
  return
831
1284
  end
832
1285
 
1286
+ # Get all views for the checkbox list
1287
+ @views = @api.views || []
1288
+
833
1289
  # Set as current view
834
1290
  @api.view(view_name)
835
- @current_view = @api.current_view
1291
+ # @current_view = @api.current_view # This line is now redundant as @current_view is set above
1292
+
1293
+ # Auto-generate view if not already generated
1294
+ index_file = @api.root/"views"/view_name/"output"/"index.html"
1295
+ unless File.exist?(index_file)
1296
+ begin
1297
+ @api.generate_view(view_name)
1298
+ rescue => e
1299
+ # Log the error but don't fail the dashboard load
1300
+ File.write('/tmp/dashboard_debug.log', "Auto-generation failed: #{e.message}\n", mode: 'a')
1301
+ end
1302
+ end
836
1303
 
837
1304
  # Generate banner for display
838
1305
  begin
839
- bsvg = Scriptorium::BannerSVG.new(view.title, view.subtitle)
1306
+ bsvg = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
840
1307
  svg_config_file = @api.root/"views"/view_name/"config"/"svg.txt"
841
1308
  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
1309
+ bsvg.parse_header_svg(svg_config_file)
860
1310
  else
861
1311
  bsvg.parse_header_svg
862
1312
  end
863
1313
  # 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
1314
+ svg_html = bsvg.get_svg
1315
+ File.write('/tmp/dashboard_debug.log', "get_svg returned: #{svg_html[0..200]}...\n", mode: 'a')
1316
+ @banner_svg = svg_html
883
1317
  rescue => e
884
1318
  @banner_svg = "<p>Error generating banner: #{e.message}</p>"
885
1319
  end
886
1320
 
1321
+ # Get posts for pagination
1322
+ begin
1323
+ posts = @api.posts(view_name, include_deleted: true) || []
1324
+
1325
+ # Debug: check if include_deleted is working
1326
+ File.write('/tmp/dashboard_debug.log', "Found #{posts.length} posts (including deleted)\n", mode: 'a')
1327
+ deleted_count = posts.count(&:deleted)
1328
+ File.write('/tmp/dashboard_debug.log', "Deleted posts: #{deleted_count}\n", mode: 'a')
1329
+
1330
+ # Debug: log first few posts and their dates for ordering analysis
1331
+ posts.first(5).each_with_index do |post, i|
1332
+ File.write('/tmp/dashboard_debug.log', "Post #{i}: #{post.num} - #{post.title} - date: #{post.date}\n", mode: 'a')
1333
+ end
1334
+
1335
+ posts.sort! { |a, b| post_compare(a, b) } # Sort by date, newest first
1336
+
1337
+ # Get posts per page from config, default to 10
1338
+ config_file = @api.root/"views"/view_name/"config"/"post_index.txt"
1339
+ posts_per_page = 10
1340
+ if File.exist?(config_file)
1341
+ config_content = read_file(config_file)
1342
+ if config_content.strip.length > 0
1343
+ posts_per_page = config_content.lines.first.strip.split.last.to_i
1344
+ end
1345
+ end
1346
+
1347
+ # Pagination logic
1348
+ page = (params[:page] || 1).to_i
1349
+ total_posts = posts.length
1350
+ total_pages = (total_posts.to_f / posts_per_page).ceil
1351
+
1352
+ # Debug pagination
1353
+ File.write('/tmp/dashboard_debug.log', "Page requested: #{params[:page]}, calculated: #{page}, total_pages: #{total_pages}\n", mode: 'a')
1354
+
1355
+ # Preserve current page if possible, otherwise reset to 1
1356
+ if page > total_pages && total_pages > 0
1357
+ page = total_pages
1358
+ File.write('/tmp/dashboard_debug.log', "Page adjusted to total_pages: #{page}\n", mode: 'a')
1359
+ elsif page < 1 || total_pages == 0
1360
+ page = 1
1361
+ File.write('/tmp/dashboard_debug.log', "Page reset to 1\n", mode: 'a')
1362
+ end
1363
+
1364
+ start_index = (page - 1) * posts_per_page
1365
+ end_index = [start_index + posts_per_page - 1, total_posts - 1].min
1366
+
1367
+ @posts = posts[start_index..end_index] || []
1368
+ @current_page = page
1369
+ @total_pages = total_pages
1370
+ @total_posts = total_posts
1371
+ @posts_per_page = posts_per_page
1372
+ rescue => e
1373
+ @posts = []
1374
+ @current_page = 1
1375
+ @total_pages = 1
1376
+ @total_posts = 0
1377
+ @posts_per_page = 10
1378
+ end
1379
+
887
1380
  erb :view_dashboard
888
1381
  rescue => e
889
1382
  redirect "/?error=Failed to load view dashboard: #{e.message}"
@@ -904,14 +1397,9 @@ class ScriptoriumWeb < Sinatra::Base
904
1397
  @configs = {}
905
1398
 
906
1399
  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
1400
+ status_config = @api.parse_commented_file(status_file)
1401
+ status_config.each do |key, value|
1402
+ @configs[key.to_sym] = value == 'y'
915
1403
  end
916
1404
  else
917
1405
  # Default to all 'n' if status file doesn't exist
@@ -930,16 +1418,9 @@ class ScriptoriumWeb < Sinatra::Base
930
1418
  layout_file = config_dir/"layout.txt"
931
1419
  @layout_containers = []
932
1420
  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
1421
+ layout_config = @api.parse_commented_file(layout_file)
1422
+ layout_config.each do |container, _|
1423
+ @layout_containers << container
943
1424
  end
944
1425
  end
945
1426
 
@@ -958,7 +1439,7 @@ class ScriptoriumWeb < Sinatra::Base
958
1439
  header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
959
1440
  @current_config = ""
960
1441
  if File.exist?(header_file)
961
- @current_config = File.read(header_file).strip
1442
+ @current_config = read_file(header_file).strip
962
1443
  end
963
1444
 
964
1445
  # Parse current settings
@@ -988,7 +1469,7 @@ class ScriptoriumWeb < Sinatra::Base
988
1469
  # Save the header configuration
989
1470
  header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
990
1471
  FileUtils.mkdir_p(File.dirname(header_file))
991
- File.write(header_file, header_content.join("\n") + "\n")
1472
+ write_file(header_file, header_content.join("\n") + "\n")
992
1473
 
993
1474
  # Update status
994
1475
  update_config_status(@current_view.name, "header", true)
@@ -1011,7 +1492,7 @@ class ScriptoriumWeb < Sinatra::Base
1011
1492
  deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
1012
1493
  @deploy_config = ""
1013
1494
  if File.exist?(deploy_file)
1014
- @deploy_config = File.read(deploy_file).strip
1495
+ @deploy_config = read_file(deploy_file).strip
1015
1496
  end
1016
1497
 
1017
1498
  erb :deploy_config
@@ -1031,17 +1512,168 @@ class ScriptoriumWeb < Sinatra::Base
1031
1512
  # Save the deployment configuration
1032
1513
  deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
1033
1514
  FileUtils.mkdir_p(File.dirname(deploy_file))
1034
- File.write(deploy_file, deploy_config + "\n")
1515
+ write_file(deploy_file, deploy_config + "\n")
1035
1516
 
1036
1517
  # Update status
1037
1518
  update_config_status(@current_view.name, "deploy", true)
1038
1519
 
1039
- redirect "/advanced_config?message=Deployment configuration updated successfully"
1520
+ # Check if user came from deploy button - if so, auto-deploy
1521
+ if params[:from_deploy] == "1"
1522
+ # User came from deploy button, perform deployment automatically
1523
+ begin
1524
+ # Log deployment attempt
1525
+ File.open("/tmp/web_deploy.log", "a") do |f|
1526
+ f.puts "=== AUTO-DEPLOYMENT AFTER CONFIG #{Time.now} ==="
1527
+ f.puts " View name: #{@current_view.name}"
1528
+ f.puts " API object: #{@api.class}"
1529
+ f.puts " Repo root: #{@api.root}"
1530
+ end
1531
+
1532
+ # Perform deployment
1533
+ result = @api.deploy(@current_view.name)
1534
+
1535
+ # Log deployment result
1536
+ File.open("/tmp/web_deploy.log", "a") do |f|
1537
+ f.puts " Auto-deployment result: #{result}"
1538
+ f.puts " Auto-deployment completed successfully"
1539
+ end
1540
+
1541
+ if result
1542
+ redirect "/view/#{@current_view.name}?deploy_success=Deployment completed successfully&hide_uploading=1"
1543
+ else
1544
+ redirect "/view/#{@current_view.name}?error=Deployment failed&hide_uploading=1"
1545
+ end
1546
+ rescue => e
1547
+ # Log deployment error
1548
+ File.open("/tmp/web_deploy.log", "a") do |f|
1549
+ f.puts " Auto-deployment error: #{e.message}"
1550
+ f.puts " Backtrace: #{e.backtrace.first(5).join("\n ")}"
1551
+ end
1552
+ redirect "/view/#{@current_view.name}?error=Deployment configuration updated but deployment failed: #{e.message}&hide_uploading=1"
1553
+ end
1554
+ else
1555
+ # Normal case - user went to deploy config directly, just return to advanced config
1556
+ redirect "/advanced_config?message=Deployment configuration updated successfully"
1557
+ end
1040
1558
  rescue => e
1041
1559
  redirect "/deploy_config?error=Failed to save deployment configuration: #{e.message}"
1042
1560
  end
1043
1561
  end
1044
1562
 
1563
+ # Deploy current view
1564
+ get '/deploy' do
1565
+ @current_view = @api&.current_view
1566
+ if @current_view.nil?
1567
+ redirect "/?error=No view selected. Please select a view first."
1568
+ return
1569
+ end
1570
+
1571
+ # Check if deployment is ready
1572
+ unless @api.can_deploy?(@current_view.name)
1573
+ redirect "/deploy_config?error=View is not ready for deployment. Please configure deployment first.&from_deploy=1"
1574
+ return
1575
+ end
1576
+
1577
+ # Perform deployment directly
1578
+ begin
1579
+ # Log deployment attempt
1580
+ File.open("/tmp/web_deploy.log", "a") do |f|
1581
+ f.puts "=== WEB DEPLOYMENT ATTEMPT #{Time.now} ==="
1582
+ f.puts " View name: #{@current_view.name}"
1583
+ f.puts " API object: #{@api.class}"
1584
+ f.puts " Repo root: #{@api.root}"
1585
+ end
1586
+
1587
+ # Perform deployment
1588
+ result = @api.deploy(@current_view.name)
1589
+
1590
+ # Log deployment result
1591
+ File.open("/tmp/web_deploy.log", "a") do |f|
1592
+ f.puts " Deployment result: #{result}"
1593
+ f.puts " Deployment completed successfully"
1594
+ end
1595
+
1596
+ if result
1597
+ redirect "/view/#{@current_view.name}?deploy_success=Deployment completed successfully&hide_uploading=1"
1598
+ else
1599
+ redirect "/view/#{@current_view.name}?error=Deployment failed&hide_uploading=1"
1600
+ end
1601
+ rescue => e
1602
+ # Log deployment error
1603
+ File.open("/tmp/web_deploy.log", "a") do |f|
1604
+ f.puts " Deployment error: #{e.message}"
1605
+ f.puts " Backtrace: #{e.backtrace.first(5).join("\n ")}"
1606
+ end
1607
+ redirect "/view/#{@current_view.name}?error=Deployment failed: #{e.message}"
1608
+ end
1609
+ end
1610
+
1611
+
1612
+
1613
+ # Browse deployed view
1614
+ get '/browse' do
1615
+ @current_view = @api&.current_view
1616
+ if @current_view.nil?
1617
+ redirect "/?error=No view selected. Please select a view first."
1618
+ return
1619
+ end
1620
+
1621
+ # Check if deployment configuration exists
1622
+ deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
1623
+ unless File.exist?(deploy_file)
1624
+ redirect "/deploy_config?error=No deployment configuration found. Please configure deployment first."
1625
+ return
1626
+ end
1627
+
1628
+ # Read deployment configuration and extract domain
1629
+ deploy_config = read_file(deploy_file).strip
1630
+ if deploy_config.empty?
1631
+ redirect "/deploy_config?error=Deployment configuration is empty."
1632
+ return
1633
+ end
1634
+
1635
+ # Extract domain and path from deploy config (simple parsing)
1636
+ lines = deploy_config.split("\n")
1637
+ domain = nil
1638
+ path = nil
1639
+ lines.each do |line|
1640
+ line = line.strip
1641
+ next if line.empty? || line.start_with?('#')
1642
+ if line.match(/^(\w+)\s+(.+)$/)
1643
+ key = $1.strip
1644
+ value = $2.strip
1645
+ if key == 'proto' && value.start_with?('http')
1646
+ # Look for server and path fields to construct URL
1647
+ lines.each do |config_line|
1648
+ config_line = config_line.strip
1649
+ next if config_line.empty? || config_line.start_with?('#')
1650
+ if config_line.match(/^(\w+)\s+(.+)$/)
1651
+ config_key = $1.strip
1652
+ config_value = $2.strip
1653
+ if config_key == 'server'
1654
+ domain = "#{value}://#{config_value}"
1655
+ elsif config_key == 'path'
1656
+ path = config_value
1657
+ end
1658
+ end
1659
+ end
1660
+ break
1661
+ end
1662
+ end
1663
+ end
1664
+
1665
+ if domain
1666
+ # Append path if it exists
1667
+ if path && !path.empty?
1668
+ redirect "#{domain}/#{path}"
1669
+ else
1670
+ redirect domain
1671
+ end
1672
+ else
1673
+ redirect "/deploy_config?error=Could not extract domain from deployment configuration."
1674
+ end
1675
+ end
1676
+
1045
1677
  # Layout configuration page
1046
1678
  get '/layout_config' do
1047
1679
  @current_view = @api&.current_view
@@ -1054,7 +1686,7 @@ class ScriptoriumWeb < Sinatra::Base
1054
1686
  layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
1055
1687
  @layout_config = ""
1056
1688
  if File.exist?(layout_file)
1057
- @layout_config = File.read(layout_file).strip
1689
+ @layout_config = read_file(layout_file).strip
1058
1690
  end
1059
1691
 
1060
1692
  erb :layout_config
@@ -1074,7 +1706,7 @@ class ScriptoriumWeb < Sinatra::Base
1074
1706
  # Save the layout configuration
1075
1707
  layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
1076
1708
  FileUtils.mkdir_p(File.dirname(layout_file))
1077
- File.write(layout_file, layout_config + "\n")
1709
+ write_file(layout_file, layout_config + "\n")
1078
1710
 
1079
1711
  redirect "/advanced_config?message=Layout configuration updated successfully"
1080
1712
  rescue => e
@@ -1082,32 +1714,34 @@ class ScriptoriumWeb < Sinatra::Base
1082
1714
  end
1083
1715
  end
1084
1716
 
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"
1717
+ # Serve web app's own assets (like livetext_mode.js)
1718
+ get '/web_assets/*' do
1719
+ begin
1720
+ asset_path = params[:splat].first
1721
+ asset_file = File.join(Dir.pwd, 'ui', 'web', 'app', 'assets', asset_path)
1722
+
1723
+ puts "DEBUG: Asset path: #{asset_path}"
1724
+ puts "DEBUG: Asset file: #{asset_file}"
1725
+ puts "DEBUG: File exists: #{File.exist?(asset_file)}"
1726
+ puts "DEBUG: Is file: #{File.file?(asset_file)}"
1727
+
1728
+ if File.exist?(asset_file) && File.file?(asset_file)
1729
+ send_file asset_file
1730
+ else
1731
+ status 404
1732
+ "Web asset not found"
1733
+ end
1734
+ rescue => e
1735
+ puts "DEBUG: Exception in web_assets: #{e.class}: #{e.message}"
1736
+ puts "DEBUG: Backtrace: #{e.backtrace.first(3).join("\n")}"
1737
+ status 500
1738
+ "Internal server error: #{e.message}"
1095
1739
  end
1096
1740
  end
1097
1741
 
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
1742
+ # Static files are now served directly by Sinatra from the public_folder
1743
+ # No custom routes needed for assets
1744
+
1111
1745
 
1112
1746
  # Server status endpoint
1113
1747
  get '/status' do
@@ -1184,14 +1818,76 @@ class ScriptoriumWeb < Sinatra::Base
1184
1818
  end
1185
1819
  end
1186
1820
 
1821
+ # Get post-specific assets
1822
+ @post_assets = []
1823
+ posts_dir = @api.root/:posts
1824
+ if Dir.exist?(posts_dir)
1825
+ Dir.glob(posts_dir/"*").each do |post_dir|
1826
+ next unless Dir.exist?(post_dir)
1827
+ post_num = File.basename(post_dir)
1828
+ next unless post_num.match?(/^\d{4}$/) # Only process 4-digit post numbers
1829
+
1830
+ post_id = post_num.to_i
1831
+ assets = @api.list_assets('post', post_id)
1832
+ assets.each do |asset|
1833
+ @post_assets << asset.merge({
1834
+ post_id: post_id,
1835
+ post_title: get_post_title(post_id)
1836
+ })
1837
+ end
1838
+ end
1839
+ end
1840
+
1187
1841
  # Sort all asset lists
1188
1842
  @global_assets.sort_by! { |asset| asset[:filename] }
1189
1843
  @library_assets.sort_by! { |asset| asset[:filename] }
1190
1844
  @view_assets.sort_by! { |asset| asset[:filename] }
1845
+ @post_assets.sort_by! { |asset| [asset[:post_id], asset[:filename]] }
1191
1846
 
1192
1847
  erb :asset_management
1193
1848
  end
1194
1849
 
1850
+ # Upload post-specific asset
1851
+ post '/upload_post_asset' do
1852
+ @current_view = @api&.current_view
1853
+ if @current_view.nil?
1854
+ redirect "/?error=No view selected. Please select a view first."
1855
+ return
1856
+ end
1857
+
1858
+ begin
1859
+ post_id = params[:post_id]&.to_i
1860
+ file = params[:file]
1861
+
1862
+ if post_id.nil? || post_id <= 0
1863
+ redirect "/view/#{@current_view.name}?error=Invalid post ID"
1864
+ return
1865
+ end
1866
+
1867
+ if file.nil? || file[:tempfile].nil?
1868
+ redirect "/view/#{@current_view.name}?error=No file selected"
1869
+ return
1870
+ end
1871
+
1872
+ # Check if post exists
1873
+ post = @api.post(post_id)
1874
+ if post.nil?
1875
+ redirect "/view/#{@current_view.name}?error=Post #{post_id} not found"
1876
+ return
1877
+ end
1878
+
1879
+ filename = file[:filename]
1880
+ tempfile = file[:tempfile]
1881
+
1882
+ # Use the API to upload the asset with original filename
1883
+ target_file = @api.upload_asset(tempfile.path, 'post', post_id, filename: filename)
1884
+
1885
+ redirect "/view/#{@current_view.name}?message=Asset '#{filename}' uploaded successfully to post ##{post_id}"
1886
+ rescue => e
1887
+ redirect "/view/#{@current_view.name}?error=Failed to upload asset: #{e.message}"
1888
+ end
1889
+ end
1890
+
1195
1891
  # Upload asset
1196
1892
  post '/asset_management/upload' do
1197
1893
  @current_view = @api&.current_view
@@ -1327,12 +2023,151 @@ class ScriptoriumWeb < Sinatra::Base
1327
2023
  end
1328
2024
  end
1329
2025
 
2026
+ # Widget Management Routes
2027
+
2028
+ # List widgets for current view
2029
+ get '/widgets' do
2030
+ if @api&.instance_variable_get(:@repo) && @current_view
2031
+ @available_widgets = @api.widgets_available
2032
+ @configured_widgets = []
2033
+ @widget_containers = {}
2034
+
2035
+ @available_widgets.each do |widget|
2036
+ widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget
2037
+ if Dir.exist?(widget_dir)
2038
+ @configured_widgets << widget
2039
+ # Determine which container this widget is in
2040
+ @widget_containers[widget] = determine_widget_container(@current_view)
2041
+ end
2042
+ end
2043
+
2044
+ erb :widgets
2045
+ else
2046
+ redirect "/?error=No repository or view selected"
2047
+ end
2048
+ end
2049
+
2050
+ # Add widget to current view
2051
+ post '/add_widget' do
2052
+ if @api&.instance_variable_get(:@repo) && @current_view
2053
+ widget_name = params[:widget_name]&.strip
2054
+
2055
+ if widget_name.nil? || widget_name.empty?
2056
+ redirect "/widgets?error=Widget name required"
2057
+ return
2058
+ end
2059
+
2060
+ # Check if widget is available
2061
+ available_widgets = @api.widgets_available
2062
+ unless available_widgets.include?(widget_name)
2063
+ redirect "/widgets?error=Widget '#{widget_name}' not available"
2064
+ return
2065
+ end
2066
+
2067
+ # Check if widget is already configured
2068
+ widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
2069
+ if Dir.exist?(widget_dir)
2070
+ redirect "/widgets?error=Widget '#{widget_name}' already configured"
2071
+ return
2072
+ end
2073
+
2074
+ # Determine container (left/right) for widget placement
2075
+ container = determine_widget_container(@current_view)
2076
+ unless container
2077
+ redirect "/widgets?error=No left or right container found in layout. Add a left or right container to your layout first."
2078
+ return
2079
+ end
2080
+
2081
+ # Create widget directory and list.txt
2082
+ FileUtils.mkdir_p(widget_dir)
2083
+ list_file = widget_dir/"list.txt"
2084
+ File.write(list_file, "# Add #{widget_name} items here\n")
2085
+
2086
+ # Generate the widget after creation
2087
+ begin
2088
+ @api.generate_widget(widget_name)
2089
+ redirect "/widgets?message=Widget '#{widget_name}' added successfully to #{container} container and generated"
2090
+ rescue => e
2091
+ # Widget created but generation failed
2092
+ redirect "/widgets?message=Widget '#{widget_name}' added successfully to #{container} container, but generation failed: #{e.message}"
2093
+ end
2094
+ else
2095
+ redirect "/?error=No repository or view selected"
2096
+ end
2097
+ end
2098
+
2099
+ # Configure widget data
2100
+ get '/config_widget/:widget_name' do
2101
+ if @api&.instance_variable_get(:@repo) && @current_view
2102
+ @widget_name = params[:widget_name]
2103
+ widget_dir = @api.root/"views"/@current_view.name/"widgets"/@widget_name
2104
+
2105
+ unless Dir.exist?(widget_dir)
2106
+ redirect "/widgets?error=Widget '#{@widget_name}' not configured"
2107
+ return
2108
+ end
2109
+
2110
+ list_file = widget_dir/"list.txt"
2111
+ @widget_data = File.exist?(list_file) ? File.read(list_file) : ""
2112
+
2113
+ erb :config_widget
2114
+ else
2115
+ redirect "/?error=No repository or view selected"
2116
+ end
2117
+ end
2118
+
2119
+ # Update widget data
2120
+ post '/update_widget/:widget_name' do
2121
+ if @api&.instance_variable_get(:@repo) && @current_view
2122
+ widget_name = params[:widget_name]
2123
+ widget_data = params[:widget_data]
2124
+
2125
+ widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
2126
+ list_file = widget_dir/"list.txt"
2127
+
2128
+ File.write(list_file, widget_data)
2129
+
2130
+ # Generate the widget after updating
2131
+ begin
2132
+ @api.generate_widget(widget_name)
2133
+ redirect "/widgets?message=Widget '#{widget_name}' updated and generated successfully"
2134
+ rescue => e
2135
+ # Widget updated but generation failed
2136
+ redirect "/widgets?error=Widget '#{widget_name}' updated successfully, but generation failed: #{e.message}"
2137
+ end
2138
+ else
2139
+ redirect "/?error=No repository or view selected"
2140
+ end
2141
+ end
2142
+
2143
+ # Remove widget from current view
2144
+ post '/remove_widget' do
2145
+ if @api&.instance_variable_get(:@repo) && @current_view
2146
+ widget_name = params[:widget_name]&.strip
2147
+
2148
+ if widget_name.nil? || widget_name.empty?
2149
+ redirect "/widgets?error=Widget name required"
2150
+ return
2151
+ end
2152
+
2153
+ widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
2154
+ if Dir.exist?(widget_dir)
2155
+ FileUtils.rm_rf(widget_dir)
2156
+ redirect "/widgets?message=Widget '#{widget_name}' removed successfully"
2157
+ else
2158
+ redirect "/widgets?error=Widget '#{widget_name}' not found"
2159
+ end
2160
+ else
2161
+ redirect "/?error=No repository or view selected"
2162
+ end
2163
+ end
2164
+
1330
2165
  # Helper method to update status
1331
2166
  private def update_config_status(view_name, config_name, status)
1332
2167
  status_file = @api.root/"views"/view_name/"config"/"status.txt"
1333
2168
  return unless File.exist?(status_file)
1334
2169
 
1335
- content = File.read(status_file)
2170
+ content = read_file(status_file)
1336
2171
  lines = content.lines.map do |line|
1337
2172
  if line.strip.start_with?("#{config_name} ")
1338
2173
  "#{config_name} #{status ? 'y' : 'n'}\n"
@@ -1340,7 +2175,7 @@ class ScriptoriumWeb < Sinatra::Base
1340
2175
  line
1341
2176
  end
1342
2177
  end
1343
- File.write(status_file, lines.join)
2178
+ write_file(status_file, lines.join)
1344
2179
  end
1345
2180
 
1346
2181
  # Helper method for formatting file sizes
@@ -1352,8 +2187,20 @@ class ScriptoriumWeb < Sinatra::Base
1352
2187
  "#{(bytes / k**i.to_f).round(2)} #{sizes[i]}"
1353
2188
  end
1354
2189
 
2190
+ # Helper method to determine which container (left/right) widgets should be placed in
2191
+ private def determine_widget_container(view)
2192
+ layout_file = @api.root/"views"/view.name/"config"/"layout.txt"
2193
+ return nil unless File.exist?(layout_file)
2194
+
2195
+ layout_config = @api.parse_commented_file(layout_file)
2196
+ containers = layout_config.keys
2197
+
2198
+ # Prefer left container, fall back to right
2199
+ containers.find { |c| c == 'left' } || containers.find { |c| c == 'right' }
2200
+ end
2201
+
1355
2202
  def get_image_dimensions(file_path)
1356
- return nil unless File.exist?(file_path)
2203
+ return nil unless file_path && File.exist?(file_path)
1357
2204
 
1358
2205
  # Check if it's an image file
1359
2206
  image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
@@ -1362,17 +2209,392 @@ class ScriptoriumWeb < Sinatra::Base
1362
2209
  # Check if FastImage is available
1363
2210
  return nil unless defined?(FastImage)
1364
2211
 
2212
+ dimensions = FastImage.size(file_path)
2213
+ return dimensions ? "#{dimensions[0]}×#{dimensions[1]}" : nil
2214
+ rescue => e
2215
+ # If FastImage fails, return nil
2216
+ return nil
2217
+ end
2218
+
2219
+ def get_post_title(post_id)
2220
+ begin
2221
+ post = @api.post(post_id)
2222
+ post.title
2223
+ rescue => e
2224
+ "Post ##{post_id}"
2225
+ end
2226
+ end
2227
+
2228
+ def format_backup_age(timestamp)
2229
+ # Parse timestamp (format: YYYYMMDD-HHMMSS)
2230
+ year = timestamp[0..3].to_i
2231
+ month = timestamp[4..5].to_i
2232
+ day = timestamp[6..7].to_i
2233
+ hour = timestamp[9..10].to_i
2234
+ minute = timestamp[11..12].to_i
2235
+ second = timestamp[13..14].to_i
2236
+
2237
+ backup_time = Time.new(year, month, day, hour, minute, second)
2238
+ now = Time.now
2239
+ diff = now - backup_time
2240
+
2241
+ if diff < 60
2242
+ "#{diff.to_i} seconds ago"
2243
+ elsif diff < 3600
2244
+ "#{(diff / 60).to_i} minutes ago"
2245
+ elsif diff < 86400
2246
+ "#{(diff / 3600).to_i} hours ago"
2247
+ elsif diff < 2592000 # 30 days
2248
+ "#{(diff / 86400).to_i} days ago"
2249
+ else
2250
+ "#{(diff / 2592000).to_i} months ago"
2251
+ end
2252
+ end
2253
+
2254
+
2255
+
2256
+ # Delete a post (move to _postnum directory)
2257
+ post '/delete_post/:id' do
2258
+ post_id = params[:id]
2259
+
2260
+ begin
2261
+ # Set current view before proceeding
2262
+ @current_view = @api&.current_view
2263
+ if @current_view.nil?
2264
+ redirect "/?error=No view selected"
2265
+ return
2266
+ end
2267
+
2268
+ post = @api.post(post_id.to_i)
2269
+ if post.nil?
2270
+ redirect "/?error=Post #{post_id} not found"
2271
+ return
2272
+ end
2273
+
2274
+ # Mark as deleted in metadata
2275
+ post.deleted = true
2276
+
2277
+ # Move post directory to _postnum
2278
+ post_dir = @api.root/"posts"/post.num
2279
+ deleted_dir = @api.root/"posts"/"_#{post.num}"
2280
+
2281
+ if Dir.exist?(post_dir)
2282
+ FileUtils.mkdir_p(File.dirname(deleted_dir))
2283
+ FileUtils.mv(post_dir, deleted_dir)
2284
+ else
2285
+ redirect "/?error=Post directory #{post_dir} not found"
2286
+ return
2287
+ end
2288
+
2289
+ # Preserve current page if available
2290
+ current_page = params[:page] || request.env['HTTP_REFERER']&.match(/[?&]page=(\d+)/)&.[](1) || 1
2291
+ redirect "/view/#{@current_view.name}?page=#{current_page}&message=Post #{post_id} deleted successfully"
2292
+ rescue => e
2293
+ redirect "/?error=Failed to delete post: #{e.message}"
2294
+ end
2295
+ end
2296
+
2297
+ # Restore a deleted post
2298
+ post '/restore_post/:id' do
2299
+ post_id = params[:id]
2300
+
2301
+ begin
2302
+ # Set current view before proceeding
2303
+ @current_view = @api&.current_view
2304
+ if @current_view.nil?
2305
+ redirect "/?error=No view selected"
2306
+ return
2307
+ end
2308
+
2309
+ # Find the deleted post directory
2310
+ formatted_id = post_id.to_s.rjust(4, '0') # Ensure 4-digit format (e.g., "28" -> "0028")
2311
+ deleted_dir = @api.root/"posts"/"_#{formatted_id}"
2312
+ post_dir = @api.root/"posts"/formatted_id
2313
+
2314
+ if Dir.exist?(deleted_dir)
2315
+ # Move back to normal posts directory
2316
+ FileUtils.mv(deleted_dir, post_dir)
2317
+
2318
+ # Update metadata to mark as not deleted
2319
+ post = @api.post(post_id.to_i)
2320
+ if post
2321
+ # Debug: log both date fields before and after
2322
+ File.write('/tmp/restore_debug.log', "Restoring post #{post_id}: pubdate before = #{post.pubdate}, created before = #{post.created}\n", mode: 'a')
2323
+ post.deleted = false
2324
+ File.write('/tmp/restore_debug.log', "Restoring post #{post_id}: pubdate after = #{post.pubdate}, created after = #{post.created}\n", mode: 'a')
2325
+ end
2326
+
2327
+ # Preserve current page if available
2328
+ current_page = params[:page] || request.env['HTTP_REFERER']&.match(/[?&]page=(\d+)/)&.[](1) || 1
2329
+ redirect "/view/#{@current_view.name}?page=#{current_page}&message=Post #{post_id} restored successfully"
2330
+ else
2331
+ redirect "/?error=Deleted post #{post_id} not found"
2332
+ end
2333
+ rescue => e
2334
+ redirect "/?error=Failed to restore post: #{e.message}"
2335
+ end
2336
+ end
2337
+
2338
+ # Toggle post published status
2339
+ post '/toggle_post_status/:id' do
2340
+ post_id = params[:id]
2341
+
2342
+ begin
2343
+ # Set current view before proceeding
2344
+ @current_view = @api&.current_view
2345
+ if @current_view.nil?
2346
+ redirect "/?error=No view selected"
2347
+ return
2348
+ end
2349
+
2350
+ post = @api.post(post_id.to_i)
2351
+ if post.nil?
2352
+ redirect "/?error=Post #{post_id} not found"
2353
+ return
2354
+ end
2355
+
2356
+ # Toggle between published and unpublished
2357
+ if post.meta["post.published"] == "no" || post.meta["post.published"].nil?
2358
+ # Publish the post - only change published status, don't touch pubdate
2359
+ post.meta["post.published"] = "yes"
2360
+ post.save_metadata
2361
+ content_type :json
2362
+ { success: true, message: "Post #{post_id} published successfully", published: true }.to_json
2363
+ else
2364
+ # Unpublish the post - only change published status, don't touch pubdate
2365
+ post.meta["post.published"] = "no"
2366
+ post.save_metadata
2367
+ content_type :json
2368
+ { success: true, message: "Post #{post_id} unpublished successfully", published: false }.to_json
2369
+ end
2370
+ rescue => e
2371
+ redirect "/?error=Failed to toggle post status: #{e.message}"
2372
+ end
2373
+ end
2374
+
2375
+ # Backup management
2376
+ get '/backup_management' do
2377
+ @current_view = @api&.current_view
2378
+ if @current_view.nil?
2379
+ redirect "/?error=No view selected. Please select a view first."
2380
+ return
2381
+ end
2382
+
2383
+ begin
2384
+ @backups = @api.list_backups
2385
+ # Sort backups by timestamp (newest first)
2386
+ @backups.sort_by! { |backup| backup[:timestamp] }.reverse!
2387
+
2388
+ # Add human-readable age to each backup
2389
+ @backups.each do |backup|
2390
+ # Convert Time object to string if needed
2391
+ timestamp_str = backup[:timestamp].is_a?(Time) ? backup[:timestamp].strftime("%Y%m%d-%H%M%S") : backup[:timestamp]
2392
+ backup[:age] = format_backup_age(timestamp_str)
2393
+ # Also ensure timestamp is a string for display
2394
+ backup[:timestamp] = timestamp_str
2395
+ end
2396
+ rescue => e
2397
+ @backups = []
2398
+ @error = "Failed to load backups: #{e.message}"
2399
+ end
2400
+
2401
+ erb :backup_management
2402
+ end
2403
+
2404
+ # Create backup
2405
+ post '/backup_management/create' do
2406
+ @current_view = @api&.current_view
2407
+ if @current_view.nil?
2408
+ redirect "/backup_management?error=No view selected. Please select a view first."
2409
+ return
2410
+ end
2411
+
2412
+ begin
2413
+ type = params[:type] # 'full' or 'incr'
2414
+ description = params[:description]
2415
+
2416
+ # Validate type
2417
+ unless %w[full incr].include?(type)
2418
+ redirect "/backup_management?error=Invalid backup type. Must be 'full' or 'incr'."
2419
+ return
2420
+ end
2421
+
2422
+ # Create backup
2423
+ timestamp = @api.create_backup(type: type.to_sym, label: description)
2424
+
2425
+ redirect "/backup_management?message=Backup created successfully: #{timestamp}"
2426
+ rescue => e
2427
+ redirect "/backup_management?error=Failed to create backup: #{e.message}"
2428
+ end
2429
+ end
2430
+
2431
+ # Theme management routes
2432
+
2433
+ # Theme management page
2434
+ get '/theme_management' do
2435
+ begin
2436
+ @themes = @api.themes_available
2437
+ @system_themes = @api.system_themes
2438
+ @user_themes = @api.user_themes
2439
+ erb :theme_management
2440
+ rescue => e
2441
+ redirect "/?error=Failed to load themes: #{e.message}"
2442
+ end
2443
+ end
2444
+
2445
+ # Clone theme
2446
+ post '/theme_management/clone' do
1365
2447
  begin
1366
- dimensions = FastImage.size(file_path)
1367
- return dimensions ? "#{dimensions[0]}×#{dimensions[1]}" : nil
2448
+ source_theme = params[:source_theme]&.strip
2449
+ new_name = params[:new_name]&.strip
2450
+
2451
+ if source_theme.nil? || source_theme.empty?
2452
+ redirect "/theme_management?error=Source theme is required"
2453
+ return
2454
+ end
2455
+
2456
+ if new_name.nil? || new_name.empty?
2457
+ redirect "/theme_management?error=New theme name is required"
2458
+ return
2459
+ end
2460
+
2461
+ # Validate new name format
2462
+ unless new_name.match?(/^[a-zA-Z0-9_-]+$/)
2463
+ redirect "/theme_management?error=Theme name can only contain letters, numbers, hyphens, and underscores"
2464
+ return
2465
+ end
2466
+
2467
+ # Clone the theme
2468
+ @api.clone_theme(source_theme, new_name)
2469
+ redirect "/theme_management?message=Theme '#{new_name}' cloned successfully from '#{source_theme}'"
1368
2470
  rescue => e
1369
- # If FastImage fails, return nil
1370
- return nil
2471
+ error_info = friendly_error_message(e)
2472
+ redirect "/theme_management?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
1371
2473
  end
1372
2474
  end
2475
+
2476
+ # Delete theme (user themes only)
2477
+ post '/theme_management/delete' do
2478
+ begin
2479
+ theme_name = params[:theme_name]&.strip
2480
+
2481
+ if theme_name.nil? || theme_name.empty?
2482
+ redirect "/theme_management?error=Theme name is required"
2483
+ return
2484
+ end
2485
+
2486
+ # Check if it's a system theme
2487
+ if @api.system_themes.include?(theme_name)
2488
+ redirect "/theme_management?error=Cannot delete system theme '#{theme_name}'"
2489
+ return
2490
+ end
2491
+
2492
+ # Check if it's a user theme
2493
+ unless @api.user_themes.include?(theme_name)
2494
+ redirect "/theme_management?error=Theme '#{theme_name}' not found or not a user theme"
2495
+ return
2496
+ end
2497
+
2498
+ # Delete the theme directory
2499
+ theme_dir = @api.root/:themes/theme_name
2500
+ if Dir.exist?(theme_dir)
2501
+ FileUtils.rm_rf(theme_dir)
2502
+ redirect "/theme_management?message=Theme '#{theme_name}' deleted successfully"
2503
+ else
2504
+ redirect "/theme_management?error=Theme directory not found"
2505
+ end
2506
+ rescue => e
2507
+ redirect "/theme_management?error=Failed to delete theme: #{e.message}"
2508
+ end
2509
+ end
2510
+
2511
+ # Edit theme files
2512
+ get '/edit_theme/:theme_name' do
2513
+ begin
2514
+ theme_name = params[:theme_name]&.strip
2515
+ unless @api.theme_exists?(theme_name)
2516
+ redirect "/theme_management?error=Theme '#{theme_name}' not found."
2517
+ end
2518
+
2519
+ @theme_name = theme_name
2520
+ @theme_dir = @api.root/:themes/theme_name
2521
+
2522
+ # List all editable files within the theme directory
2523
+ @editable_files = []
2524
+ if Dir.exist?(@theme_dir)
2525
+ Dir.glob(File.join(@theme_dir, "**", "*.txt")).each do |file_path|
2526
+ relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(@theme_dir)).to_s
2527
+ @editable_files << relative_path
2528
+ end
2529
+ end
2530
+
2531
+ erb :edit_theme
2532
+ rescue => e
2533
+ "Error in edit_theme route: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
2534
+ end
2535
+ end
2536
+
2537
+ # Edit specific theme file
2538
+ get '/edit_theme/:theme_name/*' do
2539
+ theme_name = params[:theme_name]&.strip
2540
+ file_path_param = params[:splat].first
2541
+
2542
+ unless @api.theme_exists?(theme_name)
2543
+ redirect "/theme_management?error=Theme '#{theme_name}' not found."
2544
+ end
2545
+
2546
+ theme_file_path = File.join(@api.root/:themes/theme_name, file_path_param)
2547
+
2548
+ unless File.exist?(theme_file_path)
2549
+ redirect "/edit_theme/#{theme_name}?error=File '#{file_path_param}' not found in theme '#{theme_name}'."
2550
+ end
2551
+
2552
+ @theme_name = theme_name
2553
+ @file_name = file_path_param
2554
+ @file_content = File.read(theme_file_path)
2555
+
2556
+ erb :edit_theme_file
2557
+ end
2558
+
2559
+ # Save theme file
2560
+ post '/save_theme_file/:theme_name/*' do
2561
+ theme_name = params[:theme_name]&.strip
2562
+ file_path_param = params[:splat].first
2563
+ file_content = params[:content]
2564
+
2565
+ unless @api.theme_exists?(theme_name)
2566
+ redirect "/theme_management?error=Theme '#{theme_name}' not found."
2567
+ end
2568
+
2569
+ # Ensure it's a user theme if we're allowing edits
2570
+ unless @api.user_themes.include?(theme_name)
2571
+ redirect "/edit_theme/#{theme_name}?error=Cannot edit system theme files directly."
2572
+ end
2573
+
2574
+ theme_file_path = File.join(@api.root/:themes/theme_name, file_path_param)
2575
+
2576
+ unless File.exist?(theme_file_path)
2577
+ redirect "/edit_theme/#{theme_name}?error=File '#{file_path_param}' not found in theme '#{theme_name}'."
2578
+ end
2579
+
2580
+ File.write(theme_file_path, file_content)
2581
+ redirect "/edit_theme/#{theme_name}/#{file_path_param}?message=File saved successfully."
2582
+ rescue => e
2583
+ redirect "/edit_theme/#{theme_name}/#{file_path_param}?error=Failed to save file: #{e.message}"
2584
+ end
2585
+
2586
+ # Debug route to verify code is updated
2587
+ get '/debug' do
2588
+ "Server is running updated code at #{Time.now}"
2589
+ end
2590
+
2591
+
1373
2592
  end
1374
2593
 
1375
2594
  # Start the server if this file is run directly
1376
2595
  if __FILE__ == $0
1377
2596
  ScriptoriumWeb.run!
1378
- end
2597
+ end
2598
+
2599
+ # Set initial test mode from command line after class definition
2600
+ ScriptoriumWeb.test_mode = TEST_MODE