feat: Add AlexHost deployment pipeline and bot control functionality
All checks were successful
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 59s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped

- Add Gitea Actions workflow for manual AlexHost deployment
- Add docker-compose.alexhost.yml for production deployment
- Add deploy-alexhost.sh script with server-side build support
- Add Bot Control feature (Start/Stop/Restart) for remote bot management
- Add discovery control endpoint in TeleBot
- Update TeleBot with StartPollingAsync/StopPolling/RestartPollingAsync
- Fix platform architecture issues by building on target server
- Update docker-compose configurations for all environments

Deployment tested successfully:
- TeleShop: healthy at https://teleshop.silentmary.mywire.org
- TeleBot: healthy with discovery integration
- SilverPay: connectivity verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 12:33:46 +00:00
parent 0997cc8c57
commit 86f19ba044
20 changed files with 1186 additions and 37 deletions

View File

@@ -793,5 +793,131 @@ public class BotsController : Controller
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
}
// POST: Admin/Bots/StartBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StartBot(Guid id)
{
_logger.LogInformation("Start bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "start");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start bot {BotId}", id);
TempData["Error"] = $"Failed to start bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
// POST: Admin/Bots/StopBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StopBot(Guid id)
{
_logger.LogInformation("Stop bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "stop");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop bot {BotId}", id);
TempData["Error"] = $"Failed to stop bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
// POST: Admin/Bots/RestartBot/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RestartBot(Guid id)
{
_logger.LogInformation("Restart bot requested for {BotId}", id);
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
{
TempData["Error"] = "Bot not found";
return RedirectToAction(nameof(Index));
}
if (!bot.IsRemote)
{
TempData["Error"] = "Bot control is only available for remote bots";
return RedirectToAction(nameof(Details), new { id });
}
try
{
var result = await _discoveryService.ControlBotAsync(id, "restart");
if (result.Success)
{
TempData["Success"] = result.Message;
}
else
{
TempData["Error"] = result.Message;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to restart bot {BotId}", id);
TempData["Error"] = $"Failed to restart bot: {ex.Message}";
}
return RedirectToAction(nameof(Details), new { id });
}
#endregion
}

View File

@@ -58,9 +58,14 @@ public class ProductsController : Controller
if (!ModelState.IsValid)
{
var categories = await _categoryService.GetAllCategoriesAsync();
ViewData["Categories"] = categories.Where(c => c.IsActive);
// FIX: Re-populate VariantCollections for view rendering when validation fails
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
return View(model);
}
@@ -134,6 +139,11 @@ public class ProductsController : Controller
{
var categories = await _categoryService.GetAllCategoriesAsync();
ViewData["Categories"] = categories.Where(c => c.IsActive);
// FIX: Re-populate VariantCollections for view rendering when validation fails
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
ViewData["ProductId"] = id;
return View(model);
}