scriptorium 0.0.3 → 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.
- checksums.yaml +4 -4
- data/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/imagenotfound.jpg +0 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
- data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
- data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
- data/doc/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -0
- data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
- data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
- data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
- data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
- data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
- data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
- data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
- data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
- data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
- data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
- data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
- data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
- data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
- data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
- data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
- data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
- data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
- data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
- data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
- data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
- data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
- data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
- data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
- data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
- data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
- data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
- data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
- data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
- data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
- data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
- data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
- data/doc/imported/0018-ruby-puns/post.html +46 -0
- data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
- data/doc/imported/0020-fffff/metadata.txt +7 -0
- data/doc/imported/0020-fffff/post.html +24 -0
- data/doc/imported/0020-fffff/source.lt3 +12 -0
- data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
- data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
- data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
- data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
- data/doc/imported/0023-doctor-sleep/post.html +63 -0
- data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
- data/doc/imported/0024-just-a-test/metadata.txt +7 -0
- data/doc/imported/0024-just-a-test/post.html +24 -0
- data/doc/imported/0024-just-a-test/source.lt3 +12 -0
- data/doc/imported/import_summary.txt +98 -0
- data/doc/livetext-informal-spec.txt +65 -0
- data/doc/myuserdoc/ch-0.lt3 +31 -0
- data/doc/myuserdoc/ch-1.lt3 +37 -0
- data/doc/myuserdoc/ch-10.lt3 +22 -0
- data/doc/myuserdoc/ch-2.lt3 +37 -0
- data/doc/myuserdoc/ch-3.lt3 +19 -0
- data/doc/myuserdoc/ch-4.lt3 +43 -0
- data/doc/myuserdoc/ch-5.lt3 +22 -0
- data/doc/myuserdoc/ch-6.lt3 +19 -0
- data/doc/myuserdoc/ch-7.lt3 +16 -0
- data/doc/myuserdoc/ch-8.lt3 +13 -0
- data/doc/myuserdoc/ch-9.lt3 +19 -0
- data/doc/myuserdoc/tweak.rb +18 -0
- data/doc/myuserdoc/userdoc-toc.txt +88 -0
- data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
- data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
- data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
- data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
- data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
- data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
- data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
- data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
- data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
- data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
- data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
- data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
- data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
- data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
- data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
- data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
- data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
- data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
- data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
- data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
- data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
- data/doc/old-posts/0024-just-a-test.lt3 +12 -0
- data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
- data/doc/old-repo +1 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +35 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +2373 -0
- data/lib/scriptorium/banner_svg.rb +729 -0
- data/lib/scriptorium/contract.rb +34 -0
- data/lib/scriptorium/exceptions.rb +201 -1
- data/lib/scriptorium/helpers.rb +675 -0
- data/lib/scriptorium/post.rb +259 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +938 -0
- data/lib/scriptorium/standard_files.rb +149 -0
- data/lib/scriptorium/support/bootstrap/css.txt +5 -0
- data/lib/scriptorium/support/bootstrap/js.txt +4 -0
- data/lib/scriptorium/support/common_js/clipboard.js +35 -0
- data/lib/scriptorium/support/common_js/content-loader.js +187 -0
- data/lib/scriptorium/support/common_js/navigation.js +52 -0
- data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
- data/lib/scriptorium/support/config/reddit.txt +10 -0
- data/lib/scriptorium/support/config/reddit_template.txt +17 -0
- data/lib/scriptorium/support/config/social.txt +8 -0
- data/lib/scriptorium/support/highlight/css.txt +2 -0
- data/lib/scriptorium/support/highlight/custom.css +119 -0
- data/lib/scriptorium/support/highlight/js.txt +1 -0
- data/lib/scriptorium/support/post_index/config.txt +15 -0
- data/lib/scriptorium/support/post_index/style.css +55 -0
- data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
- data/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
- data/lib/scriptorium/support/templates/layout.txt +5 -0
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/lib/scriptorium/support/theme/footer.lt3 +2 -0
- data/lib/scriptorium/support/theme/header.lt3 +4 -0
- data/lib/scriptorium/support/theme/left.lt3 +3 -0
- data/lib/scriptorium/support/theme/main.lt3 +5 -0
- data/lib/scriptorium/support/theme/right.lt3 +3 -0
- data/lib/scriptorium/theme.rb +192 -0
- data/lib/scriptorium/version.rb +1 -1
- data/lib/scriptorium/view.rb +1021 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +38 -34
- data/lib/skeleton.rb +10 -1
- data/scriptorium.gemspec +17 -5
- data/test/README.md +69 -0
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +83 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +1000 -0
- data/test/config/deployment.txt +5 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +500 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/svg.txt +3 -0
- data/test/manual/banner-tests/test01.html +122 -0
- data/test/manual/banner-tests/test02.html +122 -0
- data/test/manual/banner-tests/test03.html +122 -0
- data/test/manual/banner-tests/test04.html +129 -0
- data/test/manual/banner-tests/test05.html +129 -0
- data/test/manual/banner-tests/test06.html +129 -0
- data/test/manual/banner-tests/test07.html +129 -0
- data/test/manual/banner-tests/test08.html +123 -0
- data/test/manual/banner-tests/test09.html +123 -0
- data/test/manual/banner-tests/test10.html +123 -0
- data/test/manual/banner-tests/test11.html +123 -0
- data/test/manual/banner-tests/test12.html +123 -0
- data/test/manual/banner-tests/test13.html +123 -0
- data/test/manual/banner-tests/test14.html +123 -0
- data/test/manual/banner-tests/test15.html +122 -0
- data/test/manual/banner-tests/test16.html +122 -0
- data/test/manual/banner-tests/test17.html +122 -0
- data/test/manual/banner-tests/test18.html +132 -0
- data/test/manual/banner-tests/test19.html +132 -0
- data/test/manual/banner-tests/test20.html +132 -0
- data/test/manual/banner-tests/test21.html +132 -0
- data/test/manual/banner-tests/test22.html +132 -0
- data/test/manual/banner-tests/test23.html +132 -0
- data/test/manual/banner-tests/test24.html +132 -0
- data/test/manual/banner-tests/test25.html +131 -0
- data/test/manual/banner_environment.rb +205 -0
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/test_advanced_widgets.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_basic_posts.rb +47 -0
- data/test/manual/test_layout_widgets.rb +40 -0
- data/test/manual/test_pagination.rb +24 -0
- data/test/manual/test_random_posts.rb +38 -0
- data/test/manual/test_syntax_highlighting.rb +167 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/staging/.DS_Store +0 -0
- data/test/support/preview_utils.rb +88 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +240 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +883 -0
- data/test/unit/api.rb +1776 -0
- data/test/unit/asset_management.rb +219 -0
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +69 -0
- data/test/unit/core.rb +1211 -0
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +478 -0
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +183 -0
- data/test/unit/livetext_basic.rb +57 -0
- data/test/unit/livetext_compatibility.rb +82 -0
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/permalink_copy_test.rb +211 -0
- data/test/unit/post.rb +309 -0
- data/test/unit/post_index_config_test.rb +258 -0
- data/test/unit/post_state_helpers_test.rb +137 -0
- data/test/unit/read_commented_file_test.rb +278 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +569 -0
- data/test/unit/social_test.rb +366 -0
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +498 -0
- data/test/unit/widgets.rb +669 -0
- data/test/web_integration_test.rb +231 -0
- data/test/web_test_helper.rb +218 -0
- data/test/web_workflow_test.rb +527 -0
- data/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1890 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +2600 -0
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +196 -0
- data/ui/web/app/views/asset_management.erb +645 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +154 -0
- data/ui/web/app/views/deploy_config.erb +149 -0
- data/ui/web/app/views/edit_pages.erb +363 -0
- data/ui/web/app/views/edit_post.erb +175 -0
- data/ui/web/app/views/edit_theme.erb +73 -0
- data/ui/web/app/views/edit_theme_file.erb +74 -0
- data/ui/web/app/views/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +779 -0
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +164 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -0
- metadata +434 -8
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
data/ui/web/app/app.rb
ADDED
@@ -0,0 +1,2600 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
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
|
+
|
18
|
+
require 'sinatra'
|
19
|
+
require 'sinatra/reloader' if development?
|
20
|
+
require 'fileutils'
|
21
|
+
require 'pathname'
|
22
|
+
begin
|
23
|
+
require 'fastimage'
|
24
|
+
rescue LoadError
|
25
|
+
# FastImage not available, will handle gracefully
|
26
|
+
end
|
27
|
+
require_relative '../../../lib/scriptorium'
|
28
|
+
require_relative 'error_helpers'
|
29
|
+
|
30
|
+
class ScriptoriumWeb < Sinatra::Base
|
31
|
+
include ErrorHelpers
|
32
|
+
include Scriptorium::Helpers
|
33
|
+
|
34
|
+
set :port, 4567
|
35
|
+
set :bind, '0.0.0.0'
|
36
|
+
set :views, File.join(__dir__, 'views')
|
37
|
+
set :show_exceptions, false # Disable Sinatra's default error display
|
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
|
+
|
83
|
+
# Enable reloading in development
|
84
|
+
configure :development do
|
85
|
+
register Sinatra::Reloader
|
86
|
+
end
|
87
|
+
|
88
|
+
# Global error handler
|
89
|
+
error do
|
90
|
+
error_info = friendly_error_message(env['sinatra.error'])
|
91
|
+
@error = error_info[:message]
|
92
|
+
@suggestion = error_info[:suggestion]
|
93
|
+
erb :error_page
|
94
|
+
end
|
95
|
+
|
96
|
+
# Initialize API instance
|
97
|
+
before do
|
98
|
+
begin
|
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
|
115
|
+
rescue => e
|
116
|
+
# Error in before block: #{e.message}
|
117
|
+
@api = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Main dashboard
|
122
|
+
get '/' do
|
123
|
+
@current_view = @api&.current_view
|
124
|
+
@views = @api&.views || []
|
125
|
+
begin
|
126
|
+
if @api&.instance_variable_get(:@repo)
|
127
|
+
|
128
|
+
# Only try to load posts if we have a current view
|
129
|
+
if @current_view
|
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')
|
133
|
+
if @posts.length > 0
|
134
|
+
end
|
135
|
+
else
|
136
|
+
File.write("/tmp/debug.log", "DEBUG: No current view\n", mode: 'a')
|
137
|
+
@posts = []
|
138
|
+
end
|
139
|
+
else
|
140
|
+
@posts = []
|
141
|
+
end
|
142
|
+
rescue => e
|
143
|
+
@posts = []
|
144
|
+
end
|
145
|
+
@error = @error || params[:error]
|
146
|
+
@message = params[:message]
|
147
|
+
|
148
|
+
erb :dashboard
|
149
|
+
end
|
150
|
+
|
151
|
+
# Change view
|
152
|
+
post '/change_view' do
|
153
|
+
view_name = params[:view_name]
|
154
|
+
|
155
|
+
if view_name.nil? || view_name.strip.empty?
|
156
|
+
render_dashboard(error: "No view selected")
|
157
|
+
return
|
158
|
+
end
|
159
|
+
|
160
|
+
begin
|
161
|
+
view = @api.lookup_view(view_name)
|
162
|
+
@api.view(view_name)
|
163
|
+
render_dashboard(message: "View changed successfully")
|
164
|
+
rescue => e
|
165
|
+
render_dashboard(error: error_with_location(e, "Failed to change view: #{e.message}"))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Create new repository
|
170
|
+
post '/create_repo' do
|
171
|
+
begin
|
172
|
+
@api.create_repo("scriptorium-TEST")
|
173
|
+
# After creating, open the repo
|
174
|
+
@api.open_repo("scriptorium-TEST")
|
175
|
+
redirect '/?message=Repository created successfully'
|
176
|
+
rescue => e
|
177
|
+
redirect "/?error=Failed to create repository: #{e.message}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Create new view
|
182
|
+
post '/create_view' do
|
183
|
+
begin
|
184
|
+
validate_required_params(params, :name, :title)
|
185
|
+
|
186
|
+
name = params[:name].strip
|
187
|
+
title = params[:title].strip
|
188
|
+
subtitle = params[:subtitle]&.strip || ""
|
189
|
+
|
190
|
+
@api.create_view(name, title, subtitle, theme: "standard")
|
191
|
+
redirect "/?message=View '#{name}' created successfully"
|
192
|
+
rescue => e
|
193
|
+
error_info = friendly_error_message(e)
|
194
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Create new post
|
199
|
+
post '/create_post' do
|
200
|
+
begin
|
201
|
+
validate_required_params(params, :title)
|
202
|
+
|
203
|
+
current_view = @api&.current_view
|
204
|
+
if current_view.nil?
|
205
|
+
redirect "/?error=No view selected. Please select a view first."
|
206
|
+
return
|
207
|
+
end
|
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
|
+
|
217
|
+
# Create a draft first
|
218
|
+
draft_path = @api.create_draft(
|
219
|
+
title: params[:title].strip,
|
220
|
+
body: "", # Empty body to start
|
221
|
+
views: selected_views,
|
222
|
+
tags: tags,
|
223
|
+
blurb: params[:blurb]&.strip
|
224
|
+
)
|
225
|
+
|
226
|
+
# Convert draft to post immediately
|
227
|
+
post_num = @api.finish_draft(draft_path)
|
228
|
+
# Generate the post to create meta.txt and other files
|
229
|
+
begin
|
230
|
+
@api.generate_post(post_num)
|
231
|
+
# Check if meta.txt was created
|
232
|
+
meta_file = @api.root/"posts"/"#{post_num.to_s.rjust(4, '0')}"/"meta.txt"
|
233
|
+
# Redirect back to dashboard with modal parameter to open CodeMirror editor
|
234
|
+
redirect "/view/#{current_view.name}?edit_post=#{post_num}"
|
235
|
+
rescue => e
|
236
|
+
# Log the actual error for debugging
|
237
|
+
STDERR.puts "ERROR in generate_post: #{e.class}: #{e.message}"
|
238
|
+
STDERR.puts e.backtrace.join("\n")
|
239
|
+
error_info = friendly_error_message(e)
|
240
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
241
|
+
end
|
242
|
+
rescue => e
|
243
|
+
error_info = friendly_error_message(e)
|
244
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Edit post (redirects to file editing)
|
249
|
+
post '/edit_post' do
|
250
|
+
begin
|
251
|
+
validate_required_params(params, :post_id)
|
252
|
+
|
253
|
+
unless validate_post_id(params[:post_id])
|
254
|
+
redirect "/?error=Invalid post ID&suggestion=Please provide a valid post number."
|
255
|
+
return
|
256
|
+
end
|
257
|
+
|
258
|
+
post = @api.post(params[:post_id].to_i)
|
259
|
+
if post.nil?
|
260
|
+
redirect "/?error=Post not found&suggestion=The post may have been deleted or moved."
|
261
|
+
return
|
262
|
+
end
|
263
|
+
|
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
|
271
|
+
rescue => e
|
272
|
+
error_info = friendly_error_message(e)
|
273
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
274
|
+
end
|
275
|
+
end
|
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
|
+
|
304
|
+
# Show edit post page
|
305
|
+
get '/edit_post/:id' do
|
306
|
+
post_id = params[:id]&.to_i
|
307
|
+
|
308
|
+
if post_id.nil? || post_id <= 0
|
309
|
+
redirect "/?error=Invalid post ID"
|
310
|
+
return
|
311
|
+
end
|
312
|
+
|
313
|
+
begin
|
314
|
+
@post = @api.post(post_id)
|
315
|
+
if @post.nil?
|
316
|
+
redirect "/?error=Post not found"
|
317
|
+
return
|
318
|
+
end
|
319
|
+
|
320
|
+
# Read the source file content
|
321
|
+
source_file = @api.root/"posts"/@post.num/"source.lt3"
|
322
|
+
if File.exist?(source_file)
|
323
|
+
@content = read_file(source_file)
|
324
|
+
else
|
325
|
+
@content = "# #{@post.title}\n\n"
|
326
|
+
end
|
327
|
+
|
328
|
+
# Set current view for template
|
329
|
+
@current_view = @api&.current_view
|
330
|
+
|
331
|
+
erb :edit_post
|
332
|
+
rescue => e
|
333
|
+
redirect "/?error=Failed to load post: #{e.message}"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Save edited post
|
338
|
+
post '/save_post/:id' do
|
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')
|
363
|
+
post = @api.post(post_id)
|
364
|
+
if post.nil?
|
365
|
+
File.write('/tmp/save_post_debug.log', "ERROR: Post not found\n", mode: 'a')
|
366
|
+
redirect "/?error=Post not found"
|
367
|
+
return
|
368
|
+
end
|
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
|
+
|
373
|
+
# Write the content to the source file
|
374
|
+
source_file = @api.root/"posts"/post.num/"source.lt3"
|
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')
|
378
|
+
|
379
|
+
# Generate the post after saving
|
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
|
389
|
+
|
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
|
410
|
+
rescue => e
|
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}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Generate post
|
419
|
+
post '/generate_post' do
|
420
|
+
post_id = params[:post_id]&.to_i
|
421
|
+
|
422
|
+
if post_id.nil? || post_id <= 0
|
423
|
+
redirect "/?error=Invalid post ID"
|
424
|
+
return
|
425
|
+
end
|
426
|
+
|
427
|
+
begin
|
428
|
+
post = @api.post(post_id)
|
429
|
+
if post.nil?
|
430
|
+
redirect "/?error=Post not found"
|
431
|
+
return
|
432
|
+
end
|
433
|
+
|
434
|
+
# Generate the post
|
435
|
+
@api.generate_post(post_id)
|
436
|
+
redirect "/?message=Post ##{post_id} generated successfully"
|
437
|
+
rescue => e
|
438
|
+
redirect "/?error=Failed to generate post: #{e.message}"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Generate view
|
443
|
+
post '/generate_view' do
|
444
|
+
view_name = params[:view_name]
|
445
|
+
|
446
|
+
begin
|
447
|
+
if view_name.nil? || view_name.strip.empty?
|
448
|
+
render_dashboard(error: "No view specified")
|
449
|
+
return
|
450
|
+
end
|
451
|
+
|
452
|
+
# Generate the view
|
453
|
+
@api.generate_view(view_name)
|
454
|
+
render_dashboard(message: "View '#{view_name}' generated successfully")
|
455
|
+
rescue => e
|
456
|
+
render_dashboard(error: error_with_location(e, "Failed to generate view: #{e.message}"))
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Preview view
|
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
|
481
|
+
view_name = params[:view_name]
|
482
|
+
|
483
|
+
begin
|
484
|
+
if view_name.nil? || view_name.strip.empty?
|
485
|
+
status 400
|
486
|
+
return "Bad request: missing view name"
|
487
|
+
end
|
488
|
+
|
489
|
+
# Generate the view to ensure it's up to date
|
490
|
+
@api.generate_view(view_name)
|
491
|
+
|
492
|
+
# Serve the generated index.html file
|
493
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
494
|
+
if File.exist?(index_file)
|
495
|
+
content_type :html
|
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)
|
521
|
+
else
|
522
|
+
status 404
|
523
|
+
"Not found"
|
524
|
+
end
|
525
|
+
rescue => e
|
526
|
+
status 500
|
527
|
+
"Error loading post_index: #{e.message}"
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Serve post files for preview
|
532
|
+
get '/preview/:view_name/posts/:filename' do
|
533
|
+
view_name = params[:view_name]
|
534
|
+
filename = params[:filename]
|
535
|
+
|
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]
|
569
|
+
|
570
|
+
begin
|
571
|
+
if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
|
572
|
+
status 404
|
573
|
+
return "File not found"
|
574
|
+
end
|
575
|
+
|
576
|
+
# Construct the file path
|
577
|
+
post_file = @api.root/"views"/view_name/"output"/"posts"/filename
|
578
|
+
|
579
|
+
if File.exist?(post_file)
|
580
|
+
content_type :html
|
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)
|
731
|
+
else
|
732
|
+
status 404
|
733
|
+
"File not found: #{filename}"
|
734
|
+
end
|
735
|
+
rescue => e
|
736
|
+
status 500
|
737
|
+
"Error loading file: #{e.message}"
|
738
|
+
end
|
739
|
+
end
|
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
|
+
|
797
|
+
# Show view configuration page
|
798
|
+
get '/configure_view/:name' do
|
799
|
+
begin
|
800
|
+
validate_required_params(params, :name)
|
801
|
+
|
802
|
+
unless validate_view_name(params[:name])
|
803
|
+
redirect "/?error=Invalid view name&suggestion=View names must contain only letters, numbers, hyphens, and underscores."
|
804
|
+
return
|
805
|
+
end
|
806
|
+
|
807
|
+
view = @api.lookup_view(params[:name])
|
808
|
+
if view.nil?
|
809
|
+
redirect "/?error=View not found&suggestion=The view '#{params[:name]}' does not exist. Check the view name or create it first."
|
810
|
+
return
|
811
|
+
end
|
812
|
+
|
813
|
+
@view = view
|
814
|
+
|
815
|
+
# Load view configuration safely
|
816
|
+
config_file = @api.root/"views"/params[:name]/"config.txt"
|
817
|
+
@config_content = safe_read_file(config_file, "# View configuration for #{params[:name]}\n")
|
818
|
+
|
819
|
+
# Load layout file safely
|
820
|
+
layout_file = @api.root/"views"/params[:name]/"config"/"layout.txt"
|
821
|
+
@layout_content = safe_read_file(layout_file, "# Layout configuration for #{params[:name]}\n")
|
822
|
+
|
823
|
+
erb :configure_view
|
824
|
+
rescue => e
|
825
|
+
error_info = friendly_error_message(e)
|
826
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
# Save view configuration
|
831
|
+
post '/save_view_config/:name' do
|
832
|
+
view_name = params[:name]
|
833
|
+
|
834
|
+
begin
|
835
|
+
view = @api.lookup_view(view_name)
|
836
|
+
if view.nil?
|
837
|
+
redirect "/?error=View not found"
|
838
|
+
return
|
839
|
+
end
|
840
|
+
|
841
|
+
# Step 1: Save basic view information
|
842
|
+
if params[:view_title] && params[:view_subtitle] && params[:view_theme]
|
843
|
+
config_content = "title #{params[:view_title]}\n"
|
844
|
+
config_content += "subtitle #{params[:view_subtitle]}\n"
|
845
|
+
config_content += "theme #{params[:view_theme]}\n"
|
846
|
+
|
847
|
+
config_file = @api.root/"views"/view_name/"config.txt"
|
848
|
+
write_file(config_file, config_content)
|
849
|
+
end
|
850
|
+
|
851
|
+
# Step 2: Save layout configuration
|
852
|
+
if params[:containers]
|
853
|
+
layout_content = ""
|
854
|
+
containers = Array(params[:containers])
|
855
|
+
|
856
|
+
containers.each do |container|
|
857
|
+
case container
|
858
|
+
when 'header'
|
859
|
+
layout_content += "header # Top (banner? title? navbar? etc.)\n"
|
860
|
+
when 'left'
|
861
|
+
width = params[:left_width] || "15%"
|
862
|
+
layout_content += "left #{width} # Left sidebar, #{width} width\n"
|
863
|
+
when 'main'
|
864
|
+
layout_content += "main # Main (center) container - posts/etc.\n"
|
865
|
+
when 'right'
|
866
|
+
width = params[:right_width] || "15%"
|
867
|
+
layout_content += "right #{width} # Right sidebar, #{width} width\n"
|
868
|
+
when 'footer'
|
869
|
+
layout_content += "footer # Footer (copyright? mail? social media? etc.)\n"
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
layout_file = @api.root/"views"/view_name/"config"/"layout.txt"
|
874
|
+
FileUtils.mkdir_p(File.dirname(layout_file))
|
875
|
+
write_file(layout_file, layout_content)
|
876
|
+
end
|
877
|
+
|
878
|
+
# Step 3: Save container content files
|
879
|
+
containers = Array(params[:containers])
|
880
|
+
|
881
|
+
containers.each do |container|
|
882
|
+
content_param = "#{container}_content"
|
883
|
+
if params[content_param]
|
884
|
+
content_file = @api.root/"views"/view_name/"config"/"#{container}.txt"
|
885
|
+
FileUtils.mkdir_p(File.dirname(content_file))
|
886
|
+
write_file(content_file, params[content_param])
|
887
|
+
|
888
|
+
# If this is header with "banner svg", create default svg.txt
|
889
|
+
if container == 'header' && params[content_param].strip == 'banner svg'
|
890
|
+
svg_file = @api.root/"views"/view_name/"config"/"svg.txt"
|
891
|
+
unless File.exist?(svg_file)
|
892
|
+
# Create default SVG configuration
|
893
|
+
default_svg_content = "# SVG Banner Configuration\n"
|
894
|
+
default_svg_content += "# Light gradient background with dark text\n"
|
895
|
+
default_svg_content += "back.linear #f8f9fa #e9ecef lr\n"
|
896
|
+
default_svg_content += "text.color #374151\n"
|
897
|
+
default_svg_content += "title.style bold\n"
|
898
|
+
write_file(svg_file, default_svg_content)
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
redirect "/?message=View '#{view_name}' configuration saved successfully"
|
905
|
+
rescue => e
|
906
|
+
redirect "/configure_view/#{view_name}?error=Failed to save configuration: #{e.message}"
|
907
|
+
end
|
908
|
+
end
|
909
|
+
|
910
|
+
# Banner configuration page
|
911
|
+
get '/banner_config' do
|
912
|
+
@current_view = @api&.current_view
|
913
|
+
if @current_view.nil?
|
914
|
+
redirect "/?error=No view selected. Please select a view first."
|
915
|
+
return
|
916
|
+
end
|
917
|
+
|
918
|
+
# Get current SVG config
|
919
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
920
|
+
@svg_config = File.exist?(svg_file) ? read_file(svg_file) : ""
|
921
|
+
|
922
|
+
# Generate current banner for display
|
923
|
+
begin
|
924
|
+
banner = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
|
925
|
+
|
926
|
+
# Use the same approach as View class
|
927
|
+
if @svg_config.strip.length > 0
|
928
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
929
|
+
banner.parse_header_svg(svg_file)
|
930
|
+
else
|
931
|
+
# No config, use defaults
|
932
|
+
banner.parse_header_svg
|
933
|
+
end
|
934
|
+
|
935
|
+
@banner_svg = banner.get_svg
|
936
|
+
rescue => e
|
937
|
+
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
938
|
+
end
|
939
|
+
|
940
|
+
erb :banner_config
|
941
|
+
end
|
942
|
+
|
943
|
+
# Update banner configuration
|
944
|
+
post '/banner_config' do
|
945
|
+
@current_view = @api&.current_view
|
946
|
+
if @current_view.nil?
|
947
|
+
redirect "/?error=No view selected. Please select a view first."
|
948
|
+
return
|
949
|
+
end
|
950
|
+
|
951
|
+
begin
|
952
|
+
svg_config = params[:svg_config] || ""
|
953
|
+
|
954
|
+
# Save the SVG configuration
|
955
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
956
|
+
FileUtils.mkdir_p(File.dirname(svg_file))
|
957
|
+
write_file(svg_file, svg_config)
|
958
|
+
|
959
|
+
# Update status
|
960
|
+
update_config_status(@current_view.name, "banner", true)
|
961
|
+
|
962
|
+
redirect "/banner_config?message=Banner configuration updated successfully"
|
963
|
+
rescue => e
|
964
|
+
redirect "/banner_config?error=Failed to save banner configuration: #{e.message}"
|
965
|
+
end
|
966
|
+
end
|
967
|
+
|
968
|
+
# Navbar configuration page
|
969
|
+
get '/navbar_config' do
|
970
|
+
@current_view = @api&.current_view
|
971
|
+
if @current_view.nil?
|
972
|
+
redirect "/?error=No view selected. Please select a view first."
|
973
|
+
return
|
974
|
+
end
|
975
|
+
|
976
|
+
# Get current navbar config
|
977
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
978
|
+
@navbar_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
979
|
+
|
980
|
+
# Generate current navbar preview
|
981
|
+
begin
|
982
|
+
view = @api.lookup_view(@current_view.name)
|
983
|
+
@navbar_preview = view.build_nav(nil) # nil = use default navbar.txt
|
984
|
+
rescue => e
|
985
|
+
@navbar_preview = "<p>Error generating navbar: #{e.message}</p>"
|
986
|
+
end
|
987
|
+
|
988
|
+
erb :navbar_config
|
989
|
+
end
|
990
|
+
|
991
|
+
# Add item (top-level link or parent)
|
992
|
+
post '/navbar_config/add_item' do
|
993
|
+
@current_view = @api&.current_view
|
994
|
+
if @current_view.nil?
|
995
|
+
redirect "/?error=No view selected. Please select a view first."
|
996
|
+
return
|
997
|
+
end
|
998
|
+
|
999
|
+
begin
|
1000
|
+
label = params[:label]&.strip
|
1001
|
+
filename = params[:filename]&.strip
|
1002
|
+
action = params[:action]
|
1003
|
+
|
1004
|
+
if label.nil? || label.empty?
|
1005
|
+
redirect "/navbar_config?error=Label is required"
|
1006
|
+
return
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# Read current navbar config
|
1010
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
1011
|
+
current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
1012
|
+
|
1013
|
+
# Add new item based on action
|
1014
|
+
if action == "link"
|
1015
|
+
if filename.nil? || filename.empty?
|
1016
|
+
redirect "/navbar_config?error=Filename is required for top-level links"
|
1017
|
+
return
|
1018
|
+
end
|
1019
|
+
new_line = "-#{label} #{filename}"
|
1020
|
+
message = "Added #{label} as top-level link"
|
1021
|
+
else
|
1022
|
+
new_line = "=#{label}"
|
1023
|
+
message = "Added #{label} as parent"
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
# Append to config
|
1027
|
+
updated_config = current_config.empty? ? new_line : "#{current_config.rstrip}\n#{new_line}"
|
1028
|
+
|
1029
|
+
# Save the updated configuration
|
1030
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
1031
|
+
write_file(navbar_file, updated_config.rstrip + "\n")
|
1032
|
+
|
1033
|
+
redirect "/navbar_config?message=#{message}"
|
1034
|
+
rescue => e
|
1035
|
+
redirect "/navbar_config?error=Failed to add item: #{e.message}"
|
1036
|
+
end
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
# Add child to parent
|
1040
|
+
post '/navbar_config/add_child' do
|
1041
|
+
@current_view = @api&.current_view
|
1042
|
+
if @current_view.nil?
|
1043
|
+
redirect "/?error=No view selected. Please select a view first."
|
1044
|
+
return
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
begin
|
1048
|
+
parent = params[:parent]&.strip
|
1049
|
+
label = params[:label]&.strip
|
1050
|
+
filename = params[:filename]&.strip
|
1051
|
+
|
1052
|
+
if parent.nil? || parent.empty?
|
1053
|
+
redirect "/navbar_config?error=Parent is required"
|
1054
|
+
return
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
if label.nil? || label.empty?
|
1058
|
+
redirect "/navbar_config?error=Label is required"
|
1059
|
+
return
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
if filename.nil? || filename.empty?
|
1063
|
+
redirect "/navbar_config?error=Filename is required"
|
1064
|
+
return
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# Read current navbar config
|
1068
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
1069
|
+
current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
1070
|
+
|
1071
|
+
# Find the parent and add child after it
|
1072
|
+
lines = current_config.lines
|
1073
|
+
new_lines = []
|
1074
|
+
parent_found = false
|
1075
|
+
|
1076
|
+
lines.each do |line|
|
1077
|
+
new_lines << line
|
1078
|
+
if line.strip == "=#{parent}"
|
1079
|
+
parent_found = true
|
1080
|
+
# Add child on next line
|
1081
|
+
new_lines << " #{label} #{filename}\n"
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
if !parent_found
|
1086
|
+
redirect "/navbar_config?error=Parent '#{parent}' not found"
|
1087
|
+
return
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
# Save the updated configuration
|
1091
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
1092
|
+
write_file(navbar_file, new_lines.join.rstrip + "\n")
|
1093
|
+
|
1094
|
+
redirect "/navbar_config?message=Added #{label} as child of #{parent}"
|
1095
|
+
rescue => e
|
1096
|
+
redirect "/navbar_config?error=Failed to add child: #{e.message}"
|
1097
|
+
end
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
# Save direct edit of navbar config
|
1101
|
+
post '/navbar_config/save_direct' do
|
1102
|
+
@current_view = @api&.current_view
|
1103
|
+
if @current_view.nil?
|
1104
|
+
redirect "/?error=No view selected. Please select a view first."
|
1105
|
+
return
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
begin
|
1109
|
+
config = params[:config]&.strip
|
1110
|
+
if config.nil?
|
1111
|
+
redirect "/navbar_config?error=Configuration is required"
|
1112
|
+
return
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
# Save the configuration
|
1116
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
1117
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
1118
|
+
write_file(navbar_file, config.rstrip + "\n")
|
1119
|
+
|
1120
|
+
# Check for missing pages and create them
|
1121
|
+
pages_created = []
|
1122
|
+
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
1123
|
+
FileUtils.mkdir_p(pages_dir) unless Dir.exist?(pages_dir)
|
1124
|
+
|
1125
|
+
# Parse navbar config to find page filenames
|
1126
|
+
config.lines.each do |line|
|
1127
|
+
line = line.rstrip
|
1128
|
+
next if line.empty? || line.start_with?('#')
|
1129
|
+
|
1130
|
+
# Check for top-level links (start with -)
|
1131
|
+
if line.start_with?('-')
|
1132
|
+
if line.include?(' ')
|
1133
|
+
parts = line.split(/\s{2,}/, 2)
|
1134
|
+
if parts.length >= 2
|
1135
|
+
title = parts[0].strip
|
1136
|
+
filename = parts[1].strip
|
1137
|
+
next if filename.empty?
|
1138
|
+
|
1139
|
+
# Add .lt3 extension if no extension
|
1140
|
+
filename += '.lt3' unless filename.include?('.')
|
1141
|
+
|
1142
|
+
# Check if page exists
|
1143
|
+
page_file = pages_dir/filename
|
1144
|
+
unless File.exist?(page_file)
|
1145
|
+
content = ".page_title #{title}\n\n"
|
1146
|
+
write_file(page_file, content)
|
1147
|
+
pages_created << filename
|
1148
|
+
end
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
# Check for child links (start with space)
|
1152
|
+
elsif line.start_with?(' ')
|
1153
|
+
if line.include?(' ')
|
1154
|
+
parts = line.split(/\s{2,}/, 2)
|
1155
|
+
if parts.length >= 2
|
1156
|
+
title = parts[0].strip
|
1157
|
+
filename = parts[1].strip
|
1158
|
+
next if filename.empty?
|
1159
|
+
|
1160
|
+
# Add .lt3 extension if no extension
|
1161
|
+
filename += '.lt3' unless filename.include?('.')
|
1162
|
+
|
1163
|
+
# Check if page exists
|
1164
|
+
page_file = pages_dir/filename
|
1165
|
+
unless File.exist?(page_file)
|
1166
|
+
content = ".page_title #{title}\n\n"
|
1167
|
+
write_file(page_file, content)
|
1168
|
+
pages_created << filename
|
1169
|
+
end
|
1170
|
+
end
|
1171
|
+
end
|
1172
|
+
end
|
1173
|
+
end
|
1174
|
+
|
1175
|
+
# Build success message
|
1176
|
+
message = "Configuration saved successfully"
|
1177
|
+
if pages_created.any?
|
1178
|
+
message += ". Created missing pages: #{pages_created.join(', ')}"
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
redirect "/navbar_config?message=#{message}"
|
1182
|
+
rescue => e
|
1183
|
+
redirect "/navbar_config?error=Failed to save configuration: #{e.message}"
|
1184
|
+
end
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
# Edit pages page
|
1188
|
+
get '/edit_pages' do
|
1189
|
+
@current_view = @api&.current_view
|
1190
|
+
if @current_view.nil?
|
1191
|
+
redirect "/?error=No view selected. Please select a view first."
|
1192
|
+
return
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# Get all pages in the current view
|
1196
|
+
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
1197
|
+
@pages = []
|
1198
|
+
|
1199
|
+
if Dir.exist?(pages_dir)
|
1200
|
+
Dir.glob(pages_dir/"*").each do |file|
|
1201
|
+
next unless File.file?(file)
|
1202
|
+
filename = File.basename(file)
|
1203
|
+
content = read_file(file)
|
1204
|
+
|
1205
|
+
# Extract page title from .page_title directive
|
1206
|
+
title = nil
|
1207
|
+
if content.lines.first&.strip&.start_with?('.page_title')
|
1208
|
+
title = content.lines.first.strip.sub('.page_title', '').strip
|
1209
|
+
end
|
1210
|
+
|
1211
|
+
@pages << {
|
1212
|
+
filename: filename,
|
1213
|
+
title: title,
|
1214
|
+
content: content,
|
1215
|
+
empty: content.strip.empty?
|
1216
|
+
}
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
# Sort pages alphabetically
|
1221
|
+
@pages.sort_by! { |page| page[:filename] }
|
1222
|
+
|
1223
|
+
erb :edit_pages
|
1224
|
+
end
|
1225
|
+
|
1226
|
+
# Save page content
|
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
|
+
|
1234
|
+
@current_view = @api&.current_view
|
1235
|
+
if @current_view.nil?
|
1236
|
+
File.write('/tmp/edit_pages_debug.log', "ERROR: No current view\n", mode: 'a')
|
1237
|
+
redirect "/?error=No view selected. Please select a view first."
|
1238
|
+
return
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
begin
|
1242
|
+
filename = params[:filename]&.strip
|
1243
|
+
content = params[:content]&.strip || ""
|
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
|
+
|
1248
|
+
if filename.nil? || filename.empty?
|
1249
|
+
File.write('/tmp/edit_pages_debug.log', "ERROR: Filename is empty\n", mode: 'a')
|
1250
|
+
redirect "/edit_pages?error=Filename is required"
|
1251
|
+
return
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
# Save the page
|
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')
|
1257
|
+
FileUtils.mkdir_p(pages_dir)
|
1258
|
+
page_file = pages_dir/filename
|
1259
|
+
File.write('/tmp/edit_pages_debug.log', "Page file: #{page_file}\n", mode: 'a')
|
1260
|
+
File.write(page_file, content)
|
1261
|
+
File.write('/tmp/edit_pages_debug.log', "SUCCESS: File written\n", mode: 'a')
|
1262
|
+
|
1263
|
+
redirect "/edit_pages?message=Page '#{filename}' saved successfully"
|
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')
|
1267
|
+
redirect "/edit_pages?error=Failed to save page: #{e.message}"
|
1268
|
+
end
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
# Per-view dashboard
|
1272
|
+
get '/view/:name' do
|
1273
|
+
view_name = params[:name]
|
1274
|
+
|
1275
|
+
# Debug logging
|
1276
|
+
File.write('/tmp/dashboard_debug.log', "Dashboard accessed for view: #{view_name} at #{Time.now}\n", mode: 'a')
|
1277
|
+
|
1278
|
+
begin
|
1279
|
+
# Look up the view
|
1280
|
+
@current_view = @api.lookup_view(view_name)
|
1281
|
+
if @current_view.nil?
|
1282
|
+
redirect "/?error=View '#{view_name}' not found"
|
1283
|
+
return
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
# Get all views for the checkbox list
|
1287
|
+
@views = @api.views || []
|
1288
|
+
|
1289
|
+
# Set as current view
|
1290
|
+
@api.view(view_name)
|
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
|
1303
|
+
|
1304
|
+
# Generate banner for display
|
1305
|
+
begin
|
1306
|
+
bsvg = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
|
1307
|
+
svg_config_file = @api.root/"views"/view_name/"config"/"svg.txt"
|
1308
|
+
if File.exist?(svg_config_file)
|
1309
|
+
bsvg.parse_header_svg(svg_config_file)
|
1310
|
+
else
|
1311
|
+
bsvg.parse_header_svg
|
1312
|
+
end
|
1313
|
+
# Generate responsive SVG for web display
|
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
|
1317
|
+
rescue => e
|
1318
|
+
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
1319
|
+
end
|
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
|
+
|
1380
|
+
erb :view_dashboard
|
1381
|
+
rescue => e
|
1382
|
+
redirect "/?error=Failed to load view dashboard: #{e.message}"
|
1383
|
+
end
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
# Advanced configuration page
|
1387
|
+
get '/advanced_config' do
|
1388
|
+
@current_view = @api&.current_view
|
1389
|
+
if @current_view.nil?
|
1390
|
+
redirect "/?error=No view selected. Please select a view first."
|
1391
|
+
return
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
# Read status from status.txt file
|
1395
|
+
config_dir = @api.root/"views"/@current_view.name/"config"
|
1396
|
+
status_file = config_dir/"status.txt"
|
1397
|
+
@configs = {}
|
1398
|
+
|
1399
|
+
if File.exist?(status_file)
|
1400
|
+
status_config = @api.parse_commented_file(status_file)
|
1401
|
+
status_config.each do |key, value|
|
1402
|
+
@configs[key.to_sym] = value == 'y'
|
1403
|
+
end
|
1404
|
+
else
|
1405
|
+
# Default to all 'n' if status file doesn't exist
|
1406
|
+
@configs = {
|
1407
|
+
header: false,
|
1408
|
+
banner: false,
|
1409
|
+
navbar: false,
|
1410
|
+
left: false,
|
1411
|
+
right: false,
|
1412
|
+
pages: false,
|
1413
|
+
deploy: false
|
1414
|
+
}
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
# Read layout to determine which containers exist
|
1418
|
+
layout_file = config_dir/"layout.txt"
|
1419
|
+
@layout_containers = []
|
1420
|
+
if File.exist?(layout_file)
|
1421
|
+
layout_config = @api.parse_commented_file(layout_file)
|
1422
|
+
layout_config.each do |container, _|
|
1423
|
+
@layout_containers << container
|
1424
|
+
end
|
1425
|
+
end
|
1426
|
+
|
1427
|
+
erb :advanced_config
|
1428
|
+
end
|
1429
|
+
|
1430
|
+
# Header configuration page
|
1431
|
+
get '/header_config' do
|
1432
|
+
@current_view = @api&.current_view
|
1433
|
+
if @current_view.nil?
|
1434
|
+
redirect "/?error=No view selected. Please select a view first."
|
1435
|
+
return
|
1436
|
+
end
|
1437
|
+
|
1438
|
+
# Read current header config
|
1439
|
+
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
1440
|
+
@current_config = ""
|
1441
|
+
if File.exist?(header_file)
|
1442
|
+
@current_config = read_file(header_file).strip
|
1443
|
+
end
|
1444
|
+
|
1445
|
+
# Parse current settings
|
1446
|
+
@banner_type = @current_config.include?("banner svg") ? "svg" : "image"
|
1447
|
+
@navbar_enabled = @current_config.include?("navbar")
|
1448
|
+
|
1449
|
+
erb :header_config
|
1450
|
+
end
|
1451
|
+
|
1452
|
+
# Update header configuration
|
1453
|
+
post '/header_config' do
|
1454
|
+
@current_view = @api&.current_view
|
1455
|
+
if @current_view.nil?
|
1456
|
+
redirect "/?error=No view selected. Please select a view first."
|
1457
|
+
return
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
begin
|
1461
|
+
banner_type = params[:banner_type] || "svg"
|
1462
|
+
navbar_enabled = params[:navbar_enabled] == "1"
|
1463
|
+
|
1464
|
+
# Build header.txt content
|
1465
|
+
header_content = []
|
1466
|
+
header_content << "banner #{banner_type}"
|
1467
|
+
header_content << "navbar" if navbar_enabled
|
1468
|
+
|
1469
|
+
# Save the header configuration
|
1470
|
+
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
1471
|
+
FileUtils.mkdir_p(File.dirname(header_file))
|
1472
|
+
write_file(header_file, header_content.join("\n") + "\n")
|
1473
|
+
|
1474
|
+
# Update status
|
1475
|
+
update_config_status(@current_view.name, "header", true)
|
1476
|
+
|
1477
|
+
redirect "/advanced_config?message=Header configuration updated successfully"
|
1478
|
+
rescue => e
|
1479
|
+
redirect "/header_config?error=Failed to save header configuration: #{e.message}"
|
1480
|
+
end
|
1481
|
+
end
|
1482
|
+
|
1483
|
+
# Deployment configuration page
|
1484
|
+
get '/deploy_config' do
|
1485
|
+
@current_view = @api&.current_view
|
1486
|
+
if @current_view.nil?
|
1487
|
+
redirect "/?error=No view selected. Please select a view first."
|
1488
|
+
return
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
# Read current deployment config
|
1492
|
+
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1493
|
+
@deploy_config = ""
|
1494
|
+
if File.exist?(deploy_file)
|
1495
|
+
@deploy_config = read_file(deploy_file).strip
|
1496
|
+
end
|
1497
|
+
|
1498
|
+
erb :deploy_config
|
1499
|
+
end
|
1500
|
+
|
1501
|
+
# Update deployment configuration
|
1502
|
+
post '/deploy_config' do
|
1503
|
+
@current_view = @api&.current_view
|
1504
|
+
if @current_view.nil?
|
1505
|
+
redirect "/?error=No view selected. Please select a view first."
|
1506
|
+
return
|
1507
|
+
end
|
1508
|
+
|
1509
|
+
begin
|
1510
|
+
deploy_config = params[:deploy_config] || ""
|
1511
|
+
|
1512
|
+
# Save the deployment configuration
|
1513
|
+
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1514
|
+
FileUtils.mkdir_p(File.dirname(deploy_file))
|
1515
|
+
write_file(deploy_file, deploy_config + "\n")
|
1516
|
+
|
1517
|
+
# Update status
|
1518
|
+
update_config_status(@current_view.name, "deploy", true)
|
1519
|
+
|
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
|
1558
|
+
rescue => e
|
1559
|
+
redirect "/deploy_config?error=Failed to save deployment configuration: #{e.message}"
|
1560
|
+
end
|
1561
|
+
end
|
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
|
+
|
1677
|
+
# Layout configuration page
|
1678
|
+
get '/layout_config' do
|
1679
|
+
@current_view = @api&.current_view
|
1680
|
+
if @current_view.nil?
|
1681
|
+
redirect "/?error=No view selected. Please select a view first."
|
1682
|
+
return
|
1683
|
+
end
|
1684
|
+
|
1685
|
+
# Read current layout config
|
1686
|
+
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1687
|
+
@layout_config = ""
|
1688
|
+
if File.exist?(layout_file)
|
1689
|
+
@layout_config = read_file(layout_file).strip
|
1690
|
+
end
|
1691
|
+
|
1692
|
+
erb :layout_config
|
1693
|
+
end
|
1694
|
+
|
1695
|
+
# Update layout configuration
|
1696
|
+
post '/layout_config' do
|
1697
|
+
@current_view = @api&.current_view
|
1698
|
+
if @current_view.nil?
|
1699
|
+
redirect "/?error=No view selected. Please select a view first."
|
1700
|
+
return
|
1701
|
+
end
|
1702
|
+
|
1703
|
+
begin
|
1704
|
+
layout_config = params[:layout_config] || ""
|
1705
|
+
|
1706
|
+
# Save the layout configuration
|
1707
|
+
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1708
|
+
FileUtils.mkdir_p(File.dirname(layout_file))
|
1709
|
+
write_file(layout_file, layout_config + "\n")
|
1710
|
+
|
1711
|
+
redirect "/advanced_config?message=Layout configuration updated successfully"
|
1712
|
+
rescue => e
|
1713
|
+
redirect "/layout_config?error=Failed to save layout configuration: #{e.message}"
|
1714
|
+
end
|
1715
|
+
end
|
1716
|
+
|
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}"
|
1739
|
+
end
|
1740
|
+
end
|
1741
|
+
|
1742
|
+
# Static files are now served directly by Sinatra from the public_folder
|
1743
|
+
# No custom routes needed for assets
|
1744
|
+
|
1745
|
+
|
1746
|
+
# Server status endpoint
|
1747
|
+
get '/status' do
|
1748
|
+
content_type :json
|
1749
|
+
{
|
1750
|
+
status: 'running',
|
1751
|
+
port: settings.port,
|
1752
|
+
current_view: @api.current_view&.name,
|
1753
|
+
repo_loaded: !@api.instance_variable_get(:@repo).nil?
|
1754
|
+
}.to_json
|
1755
|
+
end
|
1756
|
+
|
1757
|
+
# Asset management page
|
1758
|
+
get '/asset_management' do
|
1759
|
+
@current_view = @api&.current_view
|
1760
|
+
if @current_view.nil?
|
1761
|
+
redirect "/?error=No view selected. Please select a view first."
|
1762
|
+
return
|
1763
|
+
end
|
1764
|
+
|
1765
|
+
# Get global assets
|
1766
|
+
global_assets_dir = @api.root/"assets"
|
1767
|
+
@global_assets = []
|
1768
|
+
@library_assets = []
|
1769
|
+
|
1770
|
+
if Dir.exist?(global_assets_dir)
|
1771
|
+
Dir.glob(global_assets_dir/"*").each do |file|
|
1772
|
+
next unless File.file?(file)
|
1773
|
+
filename = File.basename(file)
|
1774
|
+
size = File.size(file)
|
1775
|
+
dimensions = get_image_dimensions(file)
|
1776
|
+
@global_assets << {
|
1777
|
+
filename: filename,
|
1778
|
+
size: size,
|
1779
|
+
path: file,
|
1780
|
+
dimensions: dimensions
|
1781
|
+
}
|
1782
|
+
end
|
1783
|
+
|
1784
|
+
# Get library assets
|
1785
|
+
library_dir = global_assets_dir/"library"
|
1786
|
+
if Dir.exist?(library_dir)
|
1787
|
+
Dir.glob(library_dir/"*").each do |file|
|
1788
|
+
next unless File.file?(file)
|
1789
|
+
filename = File.basename(file)
|
1790
|
+
size = File.size(file)
|
1791
|
+
dimensions = get_image_dimensions(file)
|
1792
|
+
@library_assets << {
|
1793
|
+
filename: filename,
|
1794
|
+
size: size,
|
1795
|
+
path: file,
|
1796
|
+
dimensions: dimensions
|
1797
|
+
}
|
1798
|
+
end
|
1799
|
+
end
|
1800
|
+
end
|
1801
|
+
|
1802
|
+
# Get view-specific assets
|
1803
|
+
view_assets_dir = @api.root/"views"/@current_view.name/"assets"
|
1804
|
+
@view_assets = []
|
1805
|
+
|
1806
|
+
if Dir.exist?(view_assets_dir)
|
1807
|
+
Dir.glob(view_assets_dir/"*").each do |file|
|
1808
|
+
next unless File.file?(file)
|
1809
|
+
filename = File.basename(file)
|
1810
|
+
size = File.size(file)
|
1811
|
+
dimensions = get_image_dimensions(file)
|
1812
|
+
@view_assets << {
|
1813
|
+
filename: filename,
|
1814
|
+
size: size,
|
1815
|
+
path: file,
|
1816
|
+
dimensions: dimensions
|
1817
|
+
}
|
1818
|
+
end
|
1819
|
+
end
|
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
|
+
|
1841
|
+
# Sort all asset lists
|
1842
|
+
@global_assets.sort_by! { |asset| asset[:filename] }
|
1843
|
+
@library_assets.sort_by! { |asset| asset[:filename] }
|
1844
|
+
@view_assets.sort_by! { |asset| asset[:filename] }
|
1845
|
+
@post_assets.sort_by! { |asset| [asset[:post_id], asset[:filename]] }
|
1846
|
+
|
1847
|
+
erb :asset_management
|
1848
|
+
end
|
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
|
+
|
1891
|
+
# Upload asset
|
1892
|
+
post '/asset_management/upload' do
|
1893
|
+
@current_view = @api&.current_view
|
1894
|
+
if @current_view.nil?
|
1895
|
+
redirect "/?error=No view selected. Please select a view first."
|
1896
|
+
return
|
1897
|
+
end
|
1898
|
+
|
1899
|
+
begin
|
1900
|
+
target = params[:target] # 'global', 'library', or 'view'
|
1901
|
+
file = params[:file]
|
1902
|
+
|
1903
|
+
if file.nil? || file[:tempfile].nil?
|
1904
|
+
redirect "/asset_management?error=No file selected"
|
1905
|
+
return
|
1906
|
+
end
|
1907
|
+
|
1908
|
+
filename = file[:filename]
|
1909
|
+
tempfile = file[:tempfile]
|
1910
|
+
|
1911
|
+
# Determine target directory
|
1912
|
+
case target
|
1913
|
+
when 'global'
|
1914
|
+
target_dir = @api.root/"assets"
|
1915
|
+
when 'library'
|
1916
|
+
target_dir = @api.root/"assets"/"library"
|
1917
|
+
when 'view'
|
1918
|
+
target_dir = @api.root/"views"/@current_view.name/"assets"
|
1919
|
+
else
|
1920
|
+
redirect "/asset_management?error=Invalid target"
|
1921
|
+
return
|
1922
|
+
end
|
1923
|
+
|
1924
|
+
# Create directory if it doesn't exist
|
1925
|
+
FileUtils.mkdir_p(target_dir)
|
1926
|
+
|
1927
|
+
# Save the file
|
1928
|
+
target_file = target_dir/filename
|
1929
|
+
FileUtils.cp(tempfile.path, target_file)
|
1930
|
+
|
1931
|
+
redirect "/asset_management?message=Asset '#{filename}' uploaded successfully to #{target}"
|
1932
|
+
rescue => e
|
1933
|
+
redirect "/asset_management?error=Failed to upload asset: #{e.message}"
|
1934
|
+
end
|
1935
|
+
end
|
1936
|
+
|
1937
|
+
# Copy asset from global to view
|
1938
|
+
post '/asset_management/copy' do
|
1939
|
+
@current_view = @api&.current_view
|
1940
|
+
if @current_view.nil?
|
1941
|
+
redirect "/?error=No view selected. Please select a view first."
|
1942
|
+
return
|
1943
|
+
end
|
1944
|
+
|
1945
|
+
begin
|
1946
|
+
source = params[:source] # 'global' or 'library'
|
1947
|
+
filename = params[:filename]
|
1948
|
+
|
1949
|
+
if filename.nil? || filename.empty?
|
1950
|
+
redirect "/asset_management?error=No filename specified"
|
1951
|
+
return
|
1952
|
+
end
|
1953
|
+
|
1954
|
+
# Determine source file
|
1955
|
+
case source
|
1956
|
+
when 'global'
|
1957
|
+
source_file = @api.root/"assets"/filename
|
1958
|
+
when 'library'
|
1959
|
+
source_file = @api.root/"assets"/"library"/filename
|
1960
|
+
else
|
1961
|
+
redirect "/asset_management?error=Invalid source"
|
1962
|
+
return
|
1963
|
+
end
|
1964
|
+
|
1965
|
+
unless File.exist?(source_file)
|
1966
|
+
redirect "/asset_management?error=Source file not found"
|
1967
|
+
return
|
1968
|
+
end
|
1969
|
+
|
1970
|
+
# Copy to view assets
|
1971
|
+
target_dir = @api.root/"views"/@current_view.name/"assets"
|
1972
|
+
FileUtils.mkdir_p(target_dir)
|
1973
|
+
target_file = target_dir/filename
|
1974
|
+
FileUtils.cp(source_file, target_file)
|
1975
|
+
|
1976
|
+
redirect "/asset_management?message=Asset '#{filename}' copied successfully to view"
|
1977
|
+
rescue => e
|
1978
|
+
redirect "/asset_management?error=Failed to copy asset: #{e.message}"
|
1979
|
+
end
|
1980
|
+
end
|
1981
|
+
|
1982
|
+
# Delete asset
|
1983
|
+
post '/asset_management/delete' do
|
1984
|
+
@current_view = @api&.current_view
|
1985
|
+
if @current_view.nil?
|
1986
|
+
redirect "/?error=No view selected. Please select a view first."
|
1987
|
+
return
|
1988
|
+
end
|
1989
|
+
|
1990
|
+
begin
|
1991
|
+
target = params[:target] # 'global', 'library', or 'view'
|
1992
|
+
filename = params[:filename]
|
1993
|
+
|
1994
|
+
if filename.nil? || filename.empty?
|
1995
|
+
redirect "/asset_management?error=No filename specified"
|
1996
|
+
return
|
1997
|
+
end
|
1998
|
+
|
1999
|
+
# Determine target file
|
2000
|
+
case target
|
2001
|
+
when 'global'
|
2002
|
+
target_file = @api.root/"assets"/filename
|
2003
|
+
when 'library'
|
2004
|
+
target_file = @api.root/"assets"/"library"/filename
|
2005
|
+
when 'view'
|
2006
|
+
target_file = @api.root/"views"/@current_view.name/"assets"/filename
|
2007
|
+
else
|
2008
|
+
redirect "/asset_management?error=Invalid target"
|
2009
|
+
return
|
2010
|
+
end
|
2011
|
+
|
2012
|
+
unless File.exist?(target_file)
|
2013
|
+
redirect "/asset_management?error=File not found"
|
2014
|
+
return
|
2015
|
+
end
|
2016
|
+
|
2017
|
+
# Delete the file
|
2018
|
+
File.delete(target_file)
|
2019
|
+
|
2020
|
+
redirect "/asset_management?message=Asset '#{filename}' deleted successfully"
|
2021
|
+
rescue => e
|
2022
|
+
redirect "/asset_management?error=Failed to delete asset: #{e.message}"
|
2023
|
+
end
|
2024
|
+
end
|
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
|
+
|
2165
|
+
# Helper method to update status
|
2166
|
+
private def update_config_status(view_name, config_name, status)
|
2167
|
+
status_file = @api.root/"views"/view_name/"config"/"status.txt"
|
2168
|
+
return unless File.exist?(status_file)
|
2169
|
+
|
2170
|
+
content = read_file(status_file)
|
2171
|
+
lines = content.lines.map do |line|
|
2172
|
+
if line.strip.start_with?("#{config_name} ")
|
2173
|
+
"#{config_name} #{status ? 'y' : 'n'}\n"
|
2174
|
+
else
|
2175
|
+
line
|
2176
|
+
end
|
2177
|
+
end
|
2178
|
+
write_file(status_file, lines.join)
|
2179
|
+
end
|
2180
|
+
|
2181
|
+
# Helper method for formatting file sizes
|
2182
|
+
def number_to_human_size(bytes)
|
2183
|
+
return '0 Bytes' if bytes == 0
|
2184
|
+
k = 1024
|
2185
|
+
sizes = ['Bytes', 'KB', 'MB', 'GB']
|
2186
|
+
i = (Math.log(bytes) / Math.log(k)).floor
|
2187
|
+
"#{(bytes / k**i.to_f).round(2)} #{sizes[i]}"
|
2188
|
+
end
|
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
|
+
|
2202
|
+
def get_image_dimensions(file_path)
|
2203
|
+
return nil unless file_path && File.exist?(file_path)
|
2204
|
+
|
2205
|
+
# Check if it's an image file
|
2206
|
+
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
|
2207
|
+
return nil unless image_extensions.any? { |ext| file_path.downcase.end_with?(ext) }
|
2208
|
+
|
2209
|
+
# Check if FastImage is available
|
2210
|
+
return nil unless defined?(FastImage)
|
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
|
2447
|
+
begin
|
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}'"
|
2470
|
+
rescue => e
|
2471
|
+
error_info = friendly_error_message(e)
|
2472
|
+
redirect "/theme_management?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
2473
|
+
end
|
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
|
+
|
2592
|
+
end
|
2593
|
+
|
2594
|
+
# Start the server if this file is run directly
|
2595
|
+
if __FILE__ == $0
|
2596
|
+
ScriptoriumWeb.run!
|
2597
|
+
end
|
2598
|
+
|
2599
|
+
# Set initial test mode from command line after class definition
|
2600
|
+
ScriptoriumWeb.test_mode = TEST_MODE
|